package membership // Per-subject data-plane access control derived from room membership (issue // 0003e, audit H4 residual). The control plane already authorizes metadata by // membership; this is the matching restriction on the NATS data plane so a // registered peer can only publish/subscribe on the subjects of the rooms it // actually belongs to — not on every subject on the bus. import ( "encoding/hex" "fmt" "github.com/enmanuel/unibus/pkg/frame" ) // clientInfraSubjects are the subjects every peer needs regardless of room // membership: the request/reply inbox space and the JetStream API (the durable // plane of persisted rooms). They are granted to all authorized peers so // request/reply and persisted-room history keep working under the subject ACL. var clientInfraSubjects = []string{"_INBOX.>", "$JS.API.>"} // SubjectACLFor returns a function that maps a signing public key (lowercase // hex) to the data-plane subjects that identity may publish and subscribe to: // the subject of every room it belongs to, plus the client infrastructure // subjects. It reads the live membership store, so the permissions reflect the // identity's rooms at the moment it connects. A decode error or a store failure // is returned as an error so the caller can fail closed (deny the connection) // rather than grant open access. // // Because NATS freezes permissions at connect time, a peer invited to a new room // after connecting must reconnect (client.RefreshSession) to pick up the new // room's subject. The bus is the authoritative directory of subjects, so an // unlisted subject is simply absent from the allow set. func SubjectACLFor(store Store) func(signPubHex string) ([]string, error) { return func(signPubHex string) ([]string, error) { pub, err := hex.DecodeString(signPubHex) if err != nil || len(pub) != 32 { return nil, fmt.Errorf("acl: malformed sign pub %q", signPubHex) } endpoint := frame.EndpointID(pub) rooms, err := store.ListRoomsForEndpoint(endpoint) if err != nil { return nil, fmt.Errorf("acl: list rooms for %s: %w", endpoint, err) } subjects := make([]string, 0, len(rooms)+len(clientInfraSubjects)) subjects = append(subjects, clientInfraSubjects...) for _, r := range rooms { subjects = append(subjects, r.Subject) } return subjects, nil } }