diff --git a/appview/oauth/handler/handler.go b/appview/oauth/handler/handler.go index 24a19cde..f522890f 100644 --- a/appview/oauth/handler/handler.go +++ b/appview/oauth/handler/handler.go @@ -336,7 +336,7 @@ func (o *OAuthHandler) addToDefaultKnot(did string) { defaultKnot := "knot1.tangled.sh" log.Printf("adding %s to default knot", did) - err := o.enforcer.AddMember(defaultKnot, did) + err := o.enforcer.AddKnotMember(defaultKnot, did) if err != nil { log.Println("failed to add user to knot1.tangled.sh: ", err) return diff --git a/appview/repo/repo.go b/appview/repo/repo.go index 65d1deb8..0c891578 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -1090,7 +1090,7 @@ func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: user := rp.oauth.GetUser(r) - knots, err := rp.enforcer.GetDomainsForUser(user.Did) + knots, err := rp.enforcer.GetKnotsForUser(user.Did) if err != nil { rp.pages.Notice(w, "repo", "Invalid user account.") return diff --git a/appview/state/knotstream.go b/appview/state/knotstream.go index 7472d0f4..79cacb47 100644 --- a/appview/state/knotstream.go +++ b/appview/state/knotstream.go @@ -71,7 +71,7 @@ func ingestRefUpdate(d *db.DB, enforcer *rbac.Enforcer, pc posthog.Client, dev b return err } - knownKnots, err := enforcer.GetDomainsForUser(record.CommitterDid) + knownKnots, err := enforcer.GetKnotsForUser(record.CommitterDid) if err != nil { return err } diff --git a/appview/state/state.go b/appview/state/state.go index aff035df..11cb93f9 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -336,7 +336,7 @@ func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { } // add basic acls for this domain - err = s.enforcer.AddDomain(domain) + err = s.enforcer.AddKnot(domain) if err != nil { log.Println("failed to setup owner of domain", err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -344,7 +344,7 @@ func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { } // add this did as owner of this domain - err = s.enforcer.AddOwner(domain, reg.ByDid) + err = s.enforcer.AddKnotOwner(domain, reg.ByDid) if err != nil { log.Println("failed to setup owner of domain", err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -409,7 +409,7 @@ func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { } } - ok, err := s.enforcer.IsServerOwner(user.Did, domain) + ok, err := s.enforcer.IsKnotOwner(user.Did, domain) isOwner := err == nil && ok p := pages.KnotParams{ @@ -528,7 +528,7 @@ func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { return } - err = s.enforcer.AddMember(domain, subjectIdentity.DID.String()) + err = s.enforcer.AddKnotMember(domain, subjectIdentity.DID.String()) if err != nil { w.Write([]byte(fmt.Sprint("failed to add member: ", err))) return @@ -576,7 +576,7 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: user := s.oauth.GetUser(r) - knots, err := s.enforcer.GetDomainsForUser(user.Did) + knots, err := s.enforcer.GetKnotsForUser(user.Did) if err != nil { s.pages.Notice(w, "repo", "Invalid user account.") return diff --git a/knotclient/events.go b/knotclient/events.go index 808ba595..7d14e555 100644 --- a/knotclient/events.go +++ b/knotclient/events.go @@ -44,10 +44,6 @@ func NewConsumerConfig() *ConsumerConfig { } } -func (cc *ConsumerConfig) AddEventSource(es EventSource) { - cc.Sources[es] = struct{}{} -} - type EventSource struct { Knot string } diff --git a/knotserver/handler.go b/knotserver/handler.go index 29124766..b536c5d1 100644 --- a/knotserver/handler.go +++ b/knotserver/handler.go @@ -46,7 +46,7 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j init: make(chan struct{}), } - err := e.AddDomain(ThisServer) + err := e.AddKnot(ThisServer) if err != nil { return nil, fmt.Errorf("failed to setup enforcer: %w", err) } diff --git a/knotserver/jetstream.go b/knotserver/jetstream.go index 16dcf975..26b5a464 100644 --- a/knotserver/jetstream.go +++ b/knotserver/jetstream.go @@ -43,7 +43,7 @@ func (h *Handle) processKnotMember(ctx context.Context, did string, record tangl return fmt.Errorf("failed to enforce permissions: %w", err) } - if err := h.e.AddMember(ThisServer, record.Subject); err != nil { + if err := h.e.AddKnotMember(ThisServer, record.Subject); err != nil { l.Error("failed to add member", "error", err) return fmt.Errorf("failed to add member: %w", err) } diff --git a/knotserver/routes.go b/knotserver/routes.go index baeff136..8b562822 100644 --- a/knotserver/routes.go +++ b/knotserver/routes.go @@ -1177,7 +1177,7 @@ func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) { } h.jc.AddDid(did) - if err := h.e.AddMember(ThisServer, did); err != nil { + if err := h.e.AddKnotMember(ThisServer, did); err != nil { l.Error("adding member", "error", err.Error()) writeError(w, err.Error(), http.StatusInternalServerError) return @@ -1312,7 +1312,7 @@ func (h *Handle) Init(w http.ResponseWriter, r *http.Request) { } h.jc.AddDid(data.Did) - if err := h.e.AddOwner(ThisServer, data.Did); err != nil { + if err := h.e.AddKnotOwner(ThisServer, data.Did); err != nil { l.Error("adding owner", "error", err.Error()) writeError(w, err.Error(), http.StatusInternalServerError) return diff --git a/rbac/rbac.go b/rbac/rbac.go index 5417a427..04ee20d5 100644 --- a/rbac/rbac.go +++ b/rbac/rbac.go @@ -2,7 +2,7 @@ package rbac import ( "database/sql" - "fmt" + "slices" "strings" adapter "github.com/Blank-Xu/sql-adapter" @@ -59,33 +59,63 @@ func NewEnforcer(path string) (*Enforcer, error) { return &Enforcer{e}, nil } -func (e *Enforcer) AddDomain(domain string) error { +func (e *Enforcer) AddKnot(knot string) error { // Add policies with patterns _, err := e.E.AddPolicies([][]string{ - {"server:owner", domain, domain, "server:invite"}, - {"server:member", domain, domain, "repo:create"}, + {"server:owner", knot, knot, "server:invite"}, + {"server:member", knot, knot, "repo:create"}, }) if err != nil { return err } // all owners are also members - _, err = e.E.AddGroupingPolicy("server:owner", "server:member", domain) + _, err = e.E.AddGroupingPolicy("server:owner", "server:member", knot) return err } -func (e *Enforcer) GetDomainsForUser(did string) ([]string, error) { - return e.E.GetDomainsForUser(did) -} +func (e *Enforcer) AddSpindle(spindle string) error { + // the internal repr for spindles is spindle:foo.com + spindle = intoSpindle(spindle) + + _, err := e.E.AddPolicies([][]string{ + {"server:owner", spindle, spindle, "server:invite"}, + }) + if err != nil { + return err + } -func (e *Enforcer) AddOwner(domain, owner string) error { - _, err := e.E.AddGroupingPolicy(owner, "server:owner", domain) + // all owners are also members + _, err = e.E.AddGroupingPolicy("server:owner", "server:member", spindle) return err } -func (e *Enforcer) AddMember(domain, member string) error { - _, err := e.E.AddGroupingPolicy(member, "server:member", domain) - return err +func (e *Enforcer) GetKnotsForUser(did string) ([]string, error) { + keepFunc := isNotSpindle + stripFunc := unSpindle + return e.getDomainsForUser(did, keepFunc, stripFunc) +} + +func (e *Enforcer) GetSpindlesForUser(did string) ([]string, error) { + keepFunc := isSpindle + stripFunc := unSpindle + return e.getDomainsForUser(did, keepFunc, stripFunc) +} + +func (e *Enforcer) AddKnotOwner(domain, owner string) error { + return e.addOwner(domain, owner) +} + +func (e *Enforcer) AddKnotMember(domain, member string) error { + return e.addMember(domain, member) +} + +func (e *Enforcer) AddSpindleOwner(domain, owner string) error { + return e.addOwner(intoSpindle(domain), owner) +} + +func (e *Enforcer) AddSpindleMember(domain, member string) error { + return e.addMember(intoSpindle(domain), member) } func repoPolicies(member, domain, repo string) [][]string { @@ -162,21 +192,52 @@ func (e *Enforcer) GetUserByRole(role, domain string) ([]string, error) { return nil, err } - return membersWithoutRoles, nil + slices.Sort(membersWithoutRoles) + return slices.Compact(membersWithoutRoles), nil } -func (e *Enforcer) isRole(user, role, domain string) (bool, error) { - return e.E.HasGroupingPolicy(user, role, domain) +func (e *Enforcer) GetUserByRoleInRepo(role, domain, repo string) ([]string, error) { + var users []string + + policies, err := e.E.GetImplicitUsersForResourceByDomain(repo, domain) + for _, p := range policies { + user := p[0] + if strings.HasPrefix(user, "did:") { + users = append(users, user) + } + } + if err != nil { + return nil, err + } + + slices.Sort(users) + return slices.Compact(users), nil } -func (e *Enforcer) IsServerOwner(user, domain string) (bool, error) { +func (e *Enforcer) IsKnotOwner(user, domain string) (bool, error) { return e.isRole(user, "server:owner", domain) } -func (e *Enforcer) IsServerMember(user, domain string) (bool, error) { +func (e *Enforcer) IsKnotMember(user, domain string) (bool, error) { return e.isRole(user, "server:member", domain) } +func (e *Enforcer) IsSpindleOwner(user, domain string) (bool, error) { + return e.isRole(user, "server:owner", intoSpindle(domain)) +} + +func (e *Enforcer) IsSpindleMember(user, domain string) (bool, error) { + return e.isRole(user, "server:member", intoSpindle(domain)) +} + +func (e *Enforcer) IsKnotInviteAllowed(user, domain string) (bool, error) { + return e.isInviteAllowed(user, domain) +} + +func (e *Enforcer) IsSpindleInviteAllowed(user, domain string) (bool, error) { + return e.isInviteAllowed(user, intoSpindle(domain)) +} + func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) { return e.E.Enforce(user, domain, repo, "repo:push") } @@ -202,12 +263,3 @@ func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string { return permissions } - -func checkRepoFormat(repo string) error { - // sanity check, repo must be of the form ownerDid/repo - if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") { - return fmt.Errorf("invalid repo: %s", repo) - } - - return nil -} diff --git a/rbac/util.go b/rbac/util.go new file mode 100644 index 00000000..bf5e0e64 --- /dev/null +++ b/rbac/util.go @@ -0,0 +1,83 @@ +package rbac + +import ( + "fmt" + "slices" + "strings" +) + +func (e *Enforcer) getDomainsForUser(did string, keepFunc func(string) bool, stripFunc func(string) string) ([]string, error) { + domains, err := e.E.GetDomainsForUser(did) + if err != nil { + return nil, err + } + + n := 0 + for _, x := range domains { + if keepFunc(x) { + domains[n] = stripFunc(x) + n++ + } + } + domains = domains[:n] + + return domains, nil +} + +func (e *Enforcer) addOwner(domain, owner string) error { + _, err := e.E.AddGroupingPolicy(owner, "server:owner", domain) + return err +} + +func (e *Enforcer) addMember(domain, member string) error { + _, err := e.E.AddGroupingPolicy(member, "server:member", domain) + return err +} + +func (e *Enforcer) isRole(user, role, domain string) (bool, error) { + roles, err := e.E.GetImplicitRolesForUser(user, domain) + if err != nil { + return false, err + } + if slices.Contains(roles, role) { + return true, nil + } + return false, nil +} + +func (e *Enforcer) isInviteAllowed(user, domain string) (bool, error) { + return e.E.Enforce(user, domain, domain, "server:invite") +} + +func checkRepoFormat(repo string) error { + // sanity check, repo must be of the form ownerDid/repo + if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") { + return fmt.Errorf("invalid repo: %s", repo) + } + + return nil +} + +const spindlePrefix = "spindle:" + +func intoSpindle(domain string) string { + if !isSpindle(domain) { + return spindlePrefix + domain + } + return domain +} + +func unSpindle(domain string) string { + if !isSpindle(domain) { + return domain + } + return strings.TrimPrefix(domain, spindlePrefix) +} + +func isSpindle(domain string) bool { + return strings.HasPrefix(domain, spindlePrefix) +} + +func isNotSpindle(domain string) bool { + return !isSpindle(domain) +}