forked from cwtch.im/cwtch
Merge pull request 'First Cut of Enhanced Permissions' (#543) from enhanced-permissions into master
Reviewed-on: cwtch.im/cwtch#543 Reviewed-by: Dan Ballard <dan@openprivacy.ca>
This commit is contained in:
commit
aaabb12b6c
|
@ -36,15 +36,13 @@ func TestContactRetryQueue(t *testing.T) {
|
|||
// progress...
|
||||
setup := false
|
||||
for !setup {
|
||||
if pinf, exists := cr.connections.Load(testOnion); exists {
|
||||
if pinf.(*contact).queued {
|
||||
if _, exists := cr.connections.Load(testOnion); exists {
|
||||
if _, exists := cr.authorizedPeers.Load(testOnion); exists {
|
||||
t.Logf("authorized")
|
||||
setup = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We should very quickly become connecting...
|
||||
time.Sleep(time.Second)
|
||||
|
|
|
@ -38,14 +38,14 @@ func (i *ImagePreviewsFunctionality) OnEvent(ev event.Event, profile peer.CwtchP
|
|||
case event.NewMessageFromPeer:
|
||||
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
|
||||
if err == nil {
|
||||
if ci.Accepted {
|
||||
if ci.GetPeerAC().RenderImages {
|
||||
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
|
||||
}
|
||||
}
|
||||
case event.NewMessageFromGroup:
|
||||
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
|
||||
if err == nil {
|
||||
if ci.Accepted {
|
||||
if ci.GetPeerAC().RenderImages {
|
||||
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ func (i *ImagePreviewsFunctionality) OnContactReceiveValue(profile peer.CwtchPee
|
|||
_, zone, path := path.GetScopeZonePath()
|
||||
if exists && zone == attr.ProfileZone && path == constants.CustomProfileImageKey {
|
||||
// We only download from accepted conversations
|
||||
if conversation.Accepted {
|
||||
if conversation.GetPeerAC().RenderImages {
|
||||
fileKey := value
|
||||
basepath := i.downloadFolder
|
||||
fsf := FunctionalityGate()
|
||||
|
@ -123,6 +123,16 @@ func (i *ImagePreviewsFunctionality) OnContactReceiveValue(profile peer.CwtchPee
|
|||
// handleImagePreviews checks settings and, if appropriate, auto-downloads any images
|
||||
func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer, ev *event.Event, conversationID, senderID int) {
|
||||
if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) {
|
||||
ci, err := profile.GetConversationInfo(senderID)
|
||||
if err != nil {
|
||||
log.Errorf("attempted to call handleImagePreviews with unknown conversation: %v", senderID)
|
||||
return
|
||||
}
|
||||
|
||||
if !ci.GetPeerAC().ShareFiles || !ci.GetPeerAC().RenderImages {
|
||||
log.Infof("refusing to autodownload files from sender: %v. conversation AC does not permit image rendering", senderID)
|
||||
return
|
||||
}
|
||||
|
||||
// Short-circuit failures
|
||||
// Don't auto-download images if the download path does not exist.
|
||||
|
@ -142,7 +152,7 @@ func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer,
|
|||
|
||||
// Now look at the image preview experiment
|
||||
var cm model.MessageWrapper
|
||||
err := json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
|
||||
err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
|
||||
if err == nil && cm.Overlay == model.OverlayFileSharing {
|
||||
log.Debugf("Received File Sharing Message")
|
||||
var fm OverlayMessage
|
||||
|
|
|
@ -67,3 +67,7 @@ const ProfileAttribute3 = "profile-attribute-3"
|
|||
|
||||
// Description is used on server contacts,
|
||||
const Description = "description"
|
||||
|
||||
// Used to store the status of acl migrations
|
||||
const ACLVersion = "acl-version"
|
||||
const ACLVersionOne = "acl-v1"
|
||||
|
|
|
@ -4,19 +4,31 @@ import (
|
|||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/model/constants"
|
||||
"encoding/json"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AccessControl is a type determining client assigned authorization to a peer
|
||||
// for a given conversation
|
||||
type AccessControl struct {
|
||||
Blocked bool // Any attempts from this handle to connect are blocked
|
||||
Blocked bool // Any attempts from this handle to connect are blocked overrides all other settings
|
||||
|
||||
// Basic Conversation Rights
|
||||
Read bool // Allows a handle to access the conversation
|
||||
Append bool // Allows a handle to append new messages to the conversation
|
||||
|
||||
AutoConnect bool // Profile should automatically try to connect with peer
|
||||
ExchangeAttributes bool // Profile should automatically exchange attributes like Name, Profile Image, etc.
|
||||
|
||||
// Extension Related Permissions
|
||||
ShareFiles bool // Allows a handle to share files to a conversation
|
||||
RenderImages bool // Indicates that certain filetypes should be autodownloaded and rendered when shared by this contact
|
||||
}
|
||||
|
||||
// DefaultP2PAccessControl - because in the year 2021, go does not support constant structs...
|
||||
// DefaultP2PAccessControl defaults to a semi-trusted peer with no access to special extensions.
|
||||
func DefaultP2PAccessControl() AccessControl {
|
||||
return AccessControl{Read: true, Append: true, Blocked: false}
|
||||
return AccessControl{Read: true, Append: true, ExchangeAttributes: true, Blocked: false,
|
||||
AutoConnect: true, ShareFiles: false, RenderImages: false}
|
||||
}
|
||||
|
||||
// AccessControlList represents an access control list for a conversation. Mapping handles to conversation
|
||||
|
@ -30,10 +42,10 @@ func (acl *AccessControlList) Serialize() []byte {
|
|||
}
|
||||
|
||||
// DeserializeAccessControlList takes in JSON and returns an AccessControlList
|
||||
func DeserializeAccessControlList(data []byte) AccessControlList {
|
||||
func DeserializeAccessControlList(data []byte) (AccessControlList, error) {
|
||||
var acl AccessControlList
|
||||
json.Unmarshal(data, &acl)
|
||||
return acl
|
||||
err := json.Unmarshal(data, &acl)
|
||||
return acl, err
|
||||
}
|
||||
|
||||
// Attributes a type-driven encapsulation of an Attribute map.
|
||||
|
@ -60,6 +72,8 @@ type Conversation struct {
|
|||
Handle string
|
||||
Attributes Attributes
|
||||
ACL AccessControlList
|
||||
|
||||
// Deprecated, please use ACL for permissions related functions
|
||||
Accepted bool
|
||||
}
|
||||
|
||||
|
@ -71,6 +85,16 @@ func (ci *Conversation) GetAttribute(scope attr.Scope, zone attr.Zone, key strin
|
|||
return "", false
|
||||
}
|
||||
|
||||
// GetPeerAC returns a suitable Access Control object for a the given peer conversation
|
||||
// If this is called for a group conversation, this method will error and return a safe default AC.
|
||||
func (ci *Conversation) GetPeerAC() AccessControl {
|
||||
if acl, exists := ci.ACL[ci.Handle]; exists {
|
||||
return acl
|
||||
}
|
||||
log.Errorf("attempted to access a Peer Access Control object from %v but peer ACL is undefined. This is likely a programming error", ci.Handle)
|
||||
return DefaultP2PAccessControl()
|
||||
}
|
||||
|
||||
// IsGroup is a helper attribute that identifies whether a conversation is a legacy group
|
||||
func (ci *Conversation) IsGroup() bool {
|
||||
if _, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)).ToString()]; exists {
|
||||
|
|
|
@ -310,7 +310,32 @@ func (cp *cwtchPeer) GenerateProtocolEngine(acn connectivity.ACN, bus event.Mana
|
|||
|
||||
authorizations := make(map[string]model.Authorization)
|
||||
for _, conversation := range conversations {
|
||||
|
||||
if tor.IsValidHostname(conversation.Handle) {
|
||||
|
||||
// if this profile does not have an ACL version, and the profile is accepted, then migrate
|
||||
// the permissions to the v1 ACL
|
||||
// migrate the old accepted AC to a new fine-grained one
|
||||
// we only do this for previously trusted connections
|
||||
// NOTE: this does not supercede global cwthch experiments settings
|
||||
// if share files is turned off globally then acl.ShareFiles will be ignored.
|
||||
if _, exists := conversation.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.ACLVersion); !exists {
|
||||
if conversation.Accepted {
|
||||
if ac, exists := conversation.ACL[conversation.Handle]; exists {
|
||||
ac.ShareFiles = true
|
||||
ac.RenderImages = true
|
||||
ac.AutoConnect = true
|
||||
ac.ExchangeAttributes = true
|
||||
conversation.ACL[conversation.Handle] = ac
|
||||
}
|
||||
|
||||
// Update the ACL Version
|
||||
cp.storage.SetConversationAttribute(conversation.ID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.ACLVersion)), constants.ACLVersionOne)
|
||||
// Store the updated ACL
|
||||
cp.storage.SetConversationACL(conversation.ID, conversation.ACL)
|
||||
}
|
||||
}
|
||||
|
||||
if conversation.ACL[conversation.Handle].Blocked {
|
||||
authorizations[conversation.Handle] = model.AuthBlocked
|
||||
} else {
|
||||
|
@ -682,6 +707,7 @@ func (cp *cwtchPeer) NewContactConversation(handle string, acl model.AccessContr
|
|||
conversationInfo, _ := cp.storage.GetConversationByHandle(handle)
|
||||
if conversationInfo == nil {
|
||||
conversationID, err := cp.storage.NewConversation(handle, model.Attributes{event.SaveHistoryKey: event.DeleteHistoryDefault}, model.AccessControlList{handle: acl}, accepted)
|
||||
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.ACLVersion)), constants.ACLVersionOne)
|
||||
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.AttrLastConnectionTime)), time.Now().Format(time.RFC3339Nano))
|
||||
cp.eventBus.Publish(event.NewEvent(event.ContactCreated, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationID), event.RemotePeer: handle}))
|
||||
return conversationID, err
|
||||
|
@ -689,6 +715,42 @@ func (cp *cwtchPeer) NewContactConversation(handle string, acl model.AccessContr
|
|||
return -1, fmt.Errorf("contact conversation already exists")
|
||||
}
|
||||
|
||||
// UpdateConversationAccessControlList is a genric ACL update method
|
||||
func (cp *cwtchPeer) UpdateConversationAccessControlList(id int, acl model.AccessControlList) error {
|
||||
return cp.storage.SetConversationACL(id, acl)
|
||||
}
|
||||
|
||||
// EnhancedUpdateConversationAccessControlList wraps UpdateConversationAccessControlList and allows updating via a serialized JSON struct
|
||||
func (cp *cwtchPeer) EnhancedUpdateConversationAccessControlList(id int, json string) error {
|
||||
_, err := cp.GetConversationInfo(id)
|
||||
if err == nil {
|
||||
acl, err := model.DeserializeAccessControlList([]byte(json))
|
||||
if err == nil {
|
||||
return cp.UpdateConversationAccessControlList(id, acl)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetConversationAccessControlList returns the access control list associated with the conversation
|
||||
func (cp *cwtchPeer) GetConversationAccessControlList(id int) (model.AccessControlList, error) {
|
||||
ci, err := cp.GetConversationInfo(id)
|
||||
if err == nil {
|
||||
return ci.ACL, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// EnhancedGetConversationAccessControlList serialzies the access control list associated with the conversation
|
||||
func (cp *cwtchPeer) EnhancedGetConversationAccessControlList(id int) (string, error) {
|
||||
ci, err := cp.GetConversationInfo(id)
|
||||
if err == nil {
|
||||
return string(ci.ACL.Serialize()), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// AcceptConversation looks up a conversation by `handle` and sets the Accepted status to `true`
|
||||
// This will cause Cwtch to auto connect to this conversation on start up
|
||||
func (cp *cwtchPeer) AcceptConversation(id int) error {
|
||||
|
@ -701,6 +763,21 @@ func (cp *cwtchPeer) AcceptConversation(id int) error {
|
|||
log.Errorf("Could not get conversation for %v: %v", id, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if ac, exists := ci.ACL[ci.Handle]; exists {
|
||||
ac.ShareFiles = true
|
||||
ac.AutoConnect = true
|
||||
ac.RenderImages = true
|
||||
ac.ExchangeAttributes = true
|
||||
ci.ACL[ci.Handle] = ac
|
||||
}
|
||||
err = cp.storage.SetConversationACL(id, ci.ACL)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Could not set conversation acl for %v: %v", id, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !ci.IsGroup() && !ci.IsServer() {
|
||||
cp.sendUpdateAuth(id, ci.Handle, ci.Accepted, ci.ACL[ci.Handle].Blocked)
|
||||
cp.PeerWithOnion(ci.Handle)
|
||||
|
@ -748,7 +825,7 @@ func (cp *cwtchPeer) UnblockConversation(id int) error {
|
|||
// TODO at some point in the future engine needs to understand ACLs not just legacy auth status
|
||||
cp.sendUpdateAuth(id, ci.Handle, ci.Accepted, ci.ACL[ci.Handle].Blocked)
|
||||
|
||||
if !ci.IsGroup() && !ci.IsServer() && ci.Accepted {
|
||||
if !ci.IsGroup() && !ci.IsServer() && ci.GetPeerAC().AutoConnect {
|
||||
cp.PeerWithOnion(ci.Handle)
|
||||
}
|
||||
|
||||
|
@ -1056,7 +1133,7 @@ func (cp *cwtchPeer) QueuePeeringWithOnion(handle string) {
|
|||
ci, err := cp.FetchConversationInfo(handle)
|
||||
if err == nil {
|
||||
lastSeen := cp.GetConversationLastSeenTime(ci.ID)
|
||||
if !ci.ACL[ci.Handle].Blocked && ci.Accepted {
|
||||
if !ci.ACL[ci.Handle].Blocked {
|
||||
cp.eventBus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: handle, event.LastSeen: lastSeen.Format(time.RFC3339Nano)}))
|
||||
}
|
||||
}
|
||||
|
@ -1316,7 +1393,7 @@ func (cp *cwtchPeer) getConnectionsSortedByLastSeen(doPeers, doServers bool) []*
|
|||
continue
|
||||
}
|
||||
} else {
|
||||
if !doPeers || !conversation.Accepted {
|
||||
if !doPeers {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
@ -1339,8 +1416,10 @@ func (cp *cwtchPeer) StartConnections(doPeers, doServers bool) {
|
|||
cp.QueueJoinServer(conversation.model.Handle)
|
||||
} else {
|
||||
log.Debugf(" QueuePeerWithOnion(%v)", conversation.model.Handle)
|
||||
if conversation.model.GetPeerAC().AutoConnect {
|
||||
cp.QueuePeeringWithOnion(conversation.model.Handle)
|
||||
}
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
@ -1508,7 +1587,7 @@ func (cp *cwtchPeer) eventHandler() {
|
|||
|
||||
log.Debugf("confo info lookup newgetval %v %v %v", onion, conversationInfo, err)
|
||||
// only accepted contacts can look up information
|
||||
if conversationInfo != nil && conversationInfo.Accepted {
|
||||
if conversationInfo != nil && conversationInfo.GetPeerAC().ExchangeAttributes {
|
||||
// Type Safe Scoped/Zoned Path
|
||||
zscope := attr.IntoScope(scope)
|
||||
zone, zpath := attr.ParseZone(zpath)
|
||||
|
@ -1540,7 +1619,7 @@ func (cp *cwtchPeer) eventHandler() {
|
|||
|
||||
conversationInfo, _ := cp.FetchConversationInfo(handle)
|
||||
// only accepted contacts can look up information
|
||||
if conversationInfo != nil && conversationInfo.Accepted {
|
||||
if conversationInfo != nil && conversationInfo.GetPeerAC().ExchangeAttributes {
|
||||
// Type Safe Scoped/Zoned Path
|
||||
zscope := attr.IntoScope(scope)
|
||||
zone, zpath := attr.ParseZone(zpath)
|
||||
|
|
|
@ -376,7 +376,12 @@ func (cps *CwtchProfileStorage) GetConversationByHandle(handle string) (*model.C
|
|||
}
|
||||
rows.Close()
|
||||
|
||||
return &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
|
||||
cacl, err := model.DeserializeAccessControlList(acl)
|
||||
if err != nil {
|
||||
log.Errorf("error deserializing ACL from database, database maybe corrupted: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return &model.Conversation{ID: id, Handle: handle, ACL: cacl, Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
|
||||
}
|
||||
|
||||
// FetchConversations returns *all* active conversations. This method should only be called
|
||||
|
@ -412,7 +417,13 @@ func (cps *CwtchProfileStorage) FetchConversations() ([]*model.Conversation, err
|
|||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
conversations = append(conversations, &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted})
|
||||
|
||||
cacl, err := model.DeserializeAccessControlList(acl)
|
||||
if err != nil {
|
||||
log.Errorf("error deserializing ACL from database, database maybe corrupted: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
conversations = append(conversations, &model.Conversation{ID: id, Handle: handle, ACL: cacl, Attributes: model.DeserializeAttributes(attributes), Accepted: accepted})
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -445,7 +456,12 @@ func (cps *CwtchProfileStorage) GetConversation(id int) (*model.Conversation, er
|
|||
}
|
||||
rows.Close()
|
||||
|
||||
return &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
|
||||
cacl, err := model.DeserializeAccessControlList(acl)
|
||||
if err != nil {
|
||||
log.Errorf("error deserializing ACL from database, database maybe corrupted: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return &model.Conversation{ID: id, Handle: handle, ACL: cacl, Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
|
||||
}
|
||||
|
||||
// AcceptConversation sets the accepted status of a conversation to true in the backing datastore
|
||||
|
|
|
@ -120,9 +120,19 @@ type CwtchPeer interface {
|
|||
ArchiveConversation(conversation int)
|
||||
GetConversationInfo(conversation int) (*model.Conversation, error)
|
||||
FetchConversationInfo(handle string) (*model.Conversation, error)
|
||||
|
||||
// API-level management of conversation access control
|
||||
UpdateConversationAccessControlList(id int, acl model.AccessControlList) error
|
||||
EnhancedUpdateConversationAccessControlList(conversation int, acjson string) error
|
||||
|
||||
GetConversationAccessControlList(conversation int) (model.AccessControlList, error)
|
||||
EnhancedGetConversationAccessControlList(conversation int) (string, error)
|
||||
|
||||
// Convieniance Functions for ACL Management
|
||||
AcceptConversation(conversation int) error
|
||||
BlockConversation(conversation int) error
|
||||
UnblockConversation(conversation int) error
|
||||
|
||||
SetConversationAttribute(conversation int, path attr.ScopedZonedPath, value string) error
|
||||
GetConversationAttribute(conversation int, path attr.ScopedZonedPath) (string, error)
|
||||
DeleteConversation(conversation int) error
|
||||
|
|
|
@ -145,10 +145,23 @@ func TestFileSharing(t *testing.T) {
|
|||
alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true)
|
||||
alice.PeerWithOnion(bob.GetOnion())
|
||||
|
||||
json, err := alice.EnhancedGetConversationAccessControlList(1)
|
||||
if err != nil {
|
||||
t.Fatalf("Error!: %v", err)
|
||||
}
|
||||
t.Logf("alice<->bob ACL: %s", json)
|
||||
|
||||
t.Logf("Waiting for alice and Bob to peer...")
|
||||
waitForPeerPeerConnection(t, alice, bob)
|
||||
alice.AcceptConversation(1)
|
||||
bob.AcceptConversation(1)
|
||||
err = alice.AcceptConversation(1)
|
||||
if err != nil {
|
||||
t.Fatalf("Error!: %v", err)
|
||||
}
|
||||
err = bob.AcceptConversation(1)
|
||||
if err != nil {
|
||||
t.Fatalf("Error!: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Alice and Bob are Connected!!")
|
||||
|
||||
filesharingFunctionality := filesharing.FunctionalityGate()
|
||||
|
|
Loading…
Reference in New Issue