github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/bitwarden/organization.go (about)

     1  package bitwarden
     2  
     3  import (
     4  	"errors"
     5  
     6  	"github.com/cozy/cozy-stack/model/bitwarden/settings"
     7  	"github.com/cozy/cozy-stack/model/instance"
     8  	"github.com/cozy/cozy-stack/pkg/consts"
     9  	"github.com/cozy/cozy-stack/pkg/couchdb"
    10  	"github.com/cozy/cozy-stack/pkg/couchdb/mango"
    11  	"github.com/cozy/cozy-stack/pkg/crypto"
    12  	"github.com/cozy/cozy-stack/pkg/metadata"
    13  )
    14  
    15  // OrgMemberStatus is a type for the status of an organization member
    16  type OrgMemberStatus int
    17  
    18  const (
    19  	// OrgMemberInvited is used when the member is invited but has not yet
    20  	// accepted the invitation.
    21  	OrgMemberInvited OrgMemberStatus = 0
    22  	// OrgMemberAccepted is used when the member is accepted but the owner has
    23  	// not yet confirmed that the fingerprint is OK.
    24  	OrgMemberAccepted OrgMemberStatus = 1
    25  	// OrgMemberConfirmed is used when the member is confirmed, and has access
    26  	// to the organization key to decrypt/encrypt ciphers.
    27  	OrgMemberConfirmed OrgMemberStatus = 2
    28  )
    29  
    30  // OrgMember is a struct for describing a member of an organization.
    31  type OrgMember struct {
    32  	UserID   string          `json:"user_id"`
    33  	Email    string          `json:"email"`
    34  	Name     string          `json:"name"`
    35  	OrgKey   string          `json:"key,omitempty"` // The organization key encrypted with the public key of the user
    36  	Status   OrgMemberStatus `json:"status"`
    37  	Owner    bool            `json:"owner,omitempty"`
    38  	ReadOnly bool            `json:"read_only,omitempty"`
    39  }
    40  
    41  // Collection is used to regroup ciphers.
    42  type Collection struct {
    43  	DocID string `json:"_id"`
    44  	Name  string `json:"name"`
    45  }
    46  
    47  // ID returns the collection identifier
    48  func (c *Collection) ID() string { return c.DocID }
    49  
    50  // Organization is used to make collections of ciphers and can be used for
    51  // sharing them with other users with cryptography mechanisms.
    52  type Organization struct {
    53  	CouchID    string                `json:"_id,omitempty"`
    54  	CouchRev   string                `json:"_rev,omitempty"`
    55  	Name       string                `json:"name"`
    56  	Members    map[string]OrgMember  `json:"members"` // the keys are the instances domains
    57  	Collection Collection            `json:"defaultCollection"`
    58  	Metadata   metadata.CozyMetadata `json:"cozyMetadata"`
    59  }
    60  
    61  // ID returns the organization identifier
    62  func (o *Organization) ID() string { return o.CouchID }
    63  
    64  // Rev returns the organization revision
    65  func (o *Organization) Rev() string { return o.CouchRev }
    66  
    67  // SetID changes the organization identifier
    68  func (o *Organization) SetID(id string) { o.CouchID = id }
    69  
    70  // SetRev changes the organization revision
    71  func (o *Organization) SetRev(rev string) { o.CouchRev = rev }
    72  
    73  // DocType returns the organization document type
    74  func (o *Organization) DocType() string { return consts.BitwardenOrganizations }
    75  
    76  // Clone implements couchdb.Doc
    77  func (o *Organization) Clone() couchdb.Doc {
    78  	cloned := *o
    79  	cloned.Members = make(map[string]OrgMember, len(o.Members))
    80  	for k, v := range o.Members {
    81  		cloned.Members[k] = v
    82  	}
    83  	return &cloned
    84  }
    85  
    86  // FindCiphers returns the ciphers for this organization.
    87  func (o *Organization) FindCiphers(inst *instance.Instance) ([]*Cipher, error) {
    88  	var ciphers []*Cipher
    89  	req := &couchdb.FindRequest{
    90  		UseIndex: "by-organization-id",
    91  		Selector: mango.Equal("organization_id", o.CouchID),
    92  		Limit:    1000,
    93  	}
    94  	err := couchdb.FindDocs(inst, consts.BitwardenCiphers, req, &ciphers)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	return ciphers, nil
    99  }
   100  
   101  // Delete will delete the organization and the ciphers inside it.
   102  func (o *Organization) Delete(inst *instance.Instance) error {
   103  	ciphers, err := o.FindCiphers(inst)
   104  	if err != nil {
   105  		return err
   106  	}
   107  	docs := make([]couchdb.Doc, len(ciphers))
   108  	for i := range ciphers {
   109  		docs[i] = ciphers[i].Clone()
   110  	}
   111  	if err := couchdb.BulkDeleteDocs(inst, consts.BitwardenCiphers, docs); err != nil {
   112  		return err
   113  	}
   114  
   115  	return couchdb.DeleteDoc(inst, o)
   116  }
   117  
   118  // GetCozyOrganization returns the organization used to store the credentials
   119  // for the konnectors running on the Cozy server.
   120  func GetCozyOrganization(inst *instance.Instance, setting *settings.Settings) (*Organization, error) {
   121  	if setting == nil || setting.PublicKey == "" {
   122  		return nil, errors.New("No public key")
   123  	}
   124  	orgKey, err := setting.OrganizationKey()
   125  	if err != nil {
   126  		inst.Logger().WithNamespace("bitwarden").
   127  			Infof("Cannot read the organization key: %s", err)
   128  		return nil, err
   129  	}
   130  	key, err := crypto.EncryptWithRSA(setting.PublicKey, orgKey)
   131  	if err != nil {
   132  		inst.Logger().WithNamespace("bitwarden").
   133  			Infof("Cannot encrypt with RSA: %s", err)
   134  		return nil, err
   135  	}
   136  
   137  	iv := crypto.GenerateRandomBytes(16)
   138  	payload := []byte(consts.BitwardenCozyCollectionName)
   139  	name, err := crypto.EncryptWithAES256HMAC(orgKey[:32], orgKey[32:], payload, iv)
   140  	if err != nil {
   141  		inst.Logger().WithNamespace("bitwarden").
   142  			Infof("Cannot encrypt with AES: %s", err)
   143  		return nil, err
   144  	}
   145  
   146  	settings, err := inst.SettingsDocument()
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	email, _ := settings.M["email"].(string)
   151  	publicName, _ := settings.M["public_name"].(string)
   152  	org := Organization{
   153  		CouchID: setting.OrganizationID,
   154  		Name:    consts.BitwardenCozyOrganizationName,
   155  		Members: map[string]OrgMember{
   156  			inst.Domain: {
   157  				UserID: inst.ID(),
   158  				Email:  email,
   159  				Name:   publicName,
   160  				OrgKey: key,
   161  				Status: OrgMemberConfirmed,
   162  				Owner:  true,
   163  			},
   164  		},
   165  		Collection: Collection{
   166  			DocID: setting.CollectionID,
   167  			Name:  name,
   168  		},
   169  	}
   170  	if setting.Metadata != nil {
   171  		org.Metadata = *setting.Metadata
   172  	}
   173  	return &org, nil
   174  }
   175  
   176  // FindAllOrganizations returns all the organizations, including the Cozy one.
   177  func FindAllOrganizations(inst *instance.Instance, setting *settings.Settings) ([]*Organization, error) {
   178  	var orgs []*Organization
   179  	req := &couchdb.AllDocsRequest{}
   180  	if err := couchdb.GetAllDocs(inst, consts.BitwardenOrganizations, req, &orgs); err != nil {
   181  		if couchdb.IsNoDatabaseError(err) {
   182  			_ = couchdb.CreateDB(inst, consts.BitwardenOrganizations)
   183  		} else {
   184  			return nil, err
   185  		}
   186  	}
   187  
   188  	cozy, err := GetCozyOrganization(inst, setting)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	orgs = append(orgs, cozy)
   193  	return orgs, nil
   194  }
   195  
   196  var _ couchdb.Doc = &Organization{}