github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/permission/permissions.go (about)

     1  // Package permission is used to store the permissions for each webapp,
     2  // konnector, sharing, etc.
     3  package permission
     4  
     5  import (
     6  	"encoding/json"
     7  	"fmt"
     8  	"net/http"
     9  	"strings"
    10  	"time"
    11  
    12  	build "github.com/cozy/cozy-stack/pkg/config"
    13  	"github.com/cozy/cozy-stack/pkg/consts"
    14  	"github.com/cozy/cozy-stack/pkg/couchdb"
    15  	"github.com/cozy/cozy-stack/pkg/couchdb/mango"
    16  	"github.com/cozy/cozy-stack/pkg/crypto"
    17  	"github.com/cozy/cozy-stack/pkg/metadata"
    18  	"github.com/cozy/cozy-stack/pkg/prefixer"
    19  	"github.com/labstack/echo/v4"
    20  )
    21  
    22  // DocTypeVersion represents the doctype version. Each time this document
    23  // structure is modified, update this value
    24  const DocTypeVersion = "1"
    25  
    26  // Permission is a storable object containing a set of rules and
    27  // several codes
    28  type Permission struct {
    29  	PID         string            `json:"_id,omitempty"`
    30  	PRev        string            `json:"_rev,omitempty"`
    31  	Type        string            `json:"type,omitempty"`
    32  	SourceID    string            `json:"source_id,omitempty"`
    33  	Permissions Set               `json:"permissions,omitempty"`
    34  	ExpiresAt   *time.Time        `json:"expires_at,omitempty"`
    35  	Codes       map[string]string `json:"codes,omitempty"`
    36  	ShortCodes  map[string]string `json:"shortcodes,omitempty"`
    37  	Password    interface{}       `json:"password,omitempty"`
    38  
    39  	Client   interface{}            `json:"-"` // Contains the *oauth.Client client pointer for Oauth permission type
    40  	Metadata *metadata.CozyMetadata `json:"cozyMetadata,omitempty"`
    41  }
    42  
    43  const (
    44  	// TypeRegister is the value of Permission.Type for the temporary permissions
    45  	// allowed by registerToken
    46  	TypeRegister = "register"
    47  
    48  	// TypeWebapp is the value of Permission.Type for an application
    49  	TypeWebapp = "app"
    50  
    51  	// TypeKonnector is the value of Permission.Type for an application
    52  	TypeKonnector = "konnector"
    53  
    54  	// TypeOauth is the value of Permission.Type for a oauth permission doc
    55  	TypeOauth = "oauth"
    56  
    57  	// TypeCLI is the value of Permission.Type for a command-line permission doc
    58  	TypeCLI = "cli"
    59  
    60  	// TypeShareByLink is the value of Permission.Type for a share (by link) permission doc
    61  	TypeShareByLink = "share"
    62  
    63  	// TypeSharePreview is the value of Permission.Type to preview a
    64  	// cozy-to-cozy sharing
    65  	TypeSharePreview = "share-preview"
    66  
    67  	// TypeShareInteract is the value of Permission.Type for reading and
    68  	// writing a note in a shared folder.
    69  	TypeShareInteract = "share-interact"
    70  )
    71  
    72  // ID implements jsonapi.Doc
    73  func (p *Permission) ID() string { return p.PID }
    74  
    75  // Rev implements jsonapi.Doc
    76  func (p *Permission) Rev() string { return p.PRev }
    77  
    78  // DocType implements jsonapi.Doc
    79  func (p *Permission) DocType() string { return consts.Permissions }
    80  
    81  // Clone implements couchdb.Doc
    82  func (p *Permission) Clone() couchdb.Doc {
    83  	cloned := *p
    84  	cloned.Codes = make(map[string]string)
    85  	cloned.ShortCodes = make(map[string]string)
    86  	if p.Metadata != nil {
    87  		cloned.Metadata = p.Metadata.Clone()
    88  	}
    89  	for k, v := range p.Codes {
    90  		cloned.Codes[k] = v
    91  	}
    92  	for k, v := range p.ShortCodes {
    93  		cloned.ShortCodes[k] = v
    94  	}
    95  	cloned.Permissions = make([]Rule, len(p.Permissions))
    96  	for i, r := range p.Permissions {
    97  		vals := r.Values
    98  		r.Values = make([]string, len(r.Values))
    99  		copy(r.Values, vals)
   100  		cloned.Permissions[i] = r
   101  	}
   102  	return &cloned
   103  }
   104  
   105  // SetID implements jsonapi.Doc
   106  func (p *Permission) SetID(id string) { p.PID = id }
   107  
   108  // SetRev implements jsonapi.Doc
   109  func (p *Permission) SetRev(rev string) { p.PRev = rev }
   110  
   111  // Expired returns true if the permissions are no longer valid
   112  func (p *Permission) Expired() bool {
   113  	if p.ExpiresAt == nil {
   114  		return false
   115  	}
   116  	return p.ExpiresAt.Before(time.Now())
   117  }
   118  
   119  // AddRules add some rules to the permission doc
   120  func (p *Permission) AddRules(rules ...Rule) {
   121  	newperms := append(p.Permissions, rules...)
   122  	p.Permissions = newperms
   123  }
   124  
   125  // RemoveRule remove a rule from the permission doc
   126  func (p *Permission) RemoveRule(rule Rule) {
   127  	newperms := p.Permissions[:0]
   128  	for _, r := range p.Permissions {
   129  		if r.Title != rule.Title {
   130  			newperms = append(newperms, r)
   131  		}
   132  	}
   133  	p.Permissions = newperms
   134  }
   135  
   136  // PatchCodes replace the permission docs codes
   137  func (p *Permission) PatchCodes(codes map[string]string) {
   138  	p.Codes = codes
   139  
   140  	// Removing associated shortcodes
   141  	if p.ShortCodes != nil {
   142  		updatedShortcodes := map[string]string{}
   143  
   144  		for codeName := range codes {
   145  			for shortcodeName, v := range p.ShortCodes {
   146  				if shortcodeName == codeName {
   147  					updatedShortcodes[shortcodeName] = v
   148  				}
   149  			}
   150  		}
   151  		p.ShortCodes = updatedShortcodes
   152  	}
   153  }
   154  
   155  // Revoke destroy a Permission
   156  func (p *Permission) Revoke(db prefixer.Prefixer) error {
   157  	return couchdb.DeleteDoc(db, p)
   158  }
   159  
   160  // CanUpdateShareByLink check if the child permissions can be updated by p
   161  // (p can be the parent or it has a superset of the permissions).
   162  func (p *Permission) CanUpdateShareByLink(child *Permission) bool {
   163  	if child.Type != TypeShareByLink {
   164  		return false
   165  	}
   166  	if p.Type != TypeWebapp && p.Type != TypeOauth {
   167  		return false
   168  	}
   169  	return child.SourceID == p.SourceID || child.Permissions.IsSubSetOf(p.Permissions)
   170  }
   171  
   172  // GetByID fetch a permission by its ID
   173  func GetByID(db prefixer.Prefixer, id string) (*Permission, error) {
   174  	perm := &Permission{}
   175  	if err := couchdb.GetDoc(db, consts.Permissions, id, perm); err != nil {
   176  		return nil, err
   177  	}
   178  	if perm.Expired() {
   179  		return nil, ErrExpiredToken
   180  	}
   181  	return perm, nil
   182  }
   183  
   184  // GetForRegisterToken create a non-persisted permissions doc with hard coded
   185  // registerToken permissions set
   186  func GetForRegisterToken() *Permission {
   187  	return &Permission{
   188  		Type: TypeRegister,
   189  		Permissions: Set{
   190  			Rule{
   191  				Verbs:  Verbs(GET),
   192  				Type:   consts.Settings,
   193  				Values: []string{consts.InstanceSettingsID},
   194  			},
   195  		},
   196  	}
   197  }
   198  
   199  // GetForCLI create a non-persisted permissions doc for the command-line
   200  func GetForCLI(claims *Claims) (*Permission, error) {
   201  	set, err := UnmarshalScopeString(claims.Scope)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  	pdoc := &Permission{
   206  		Type:        TypeCLI,
   207  		Permissions: set,
   208  	}
   209  	return pdoc, nil
   210  }
   211  
   212  // GetForWebapp retrieves the Permission doc for a given webapp
   213  func GetForWebapp(db prefixer.Prefixer, slug string) (*Permission, error) {
   214  	return getFromSource(db, TypeWebapp, consts.Apps, slug)
   215  }
   216  
   217  // GetForKonnector retrieves the Permission doc for a given konnector
   218  func GetForKonnector(db prefixer.Prefixer, slug string) (*Permission, error) {
   219  	return getFromSource(db, TypeKonnector, consts.Konnectors, slug)
   220  }
   221  
   222  // GetForSharePreview retrieves the Permission doc for a given sharing preview
   223  func GetForSharePreview(db prefixer.Prefixer, sharingID string) (*Permission, error) {
   224  	return getFromSource(db, TypeSharePreview, consts.Sharings, sharingID)
   225  }
   226  
   227  // GetForShareInteract retrieves the Permission doc for a given sharing to
   228  // read/write a note
   229  func GetForShareInteract(db prefixer.Prefixer, sharingID string) (*Permission, error) {
   230  	return getFromSource(db, TypeShareInteract, consts.Sharings, sharingID)
   231  }
   232  
   233  func getFromSource(db prefixer.Prefixer, permType, docType, slug string) (*Permission, error) {
   234  	var res []Permission
   235  	req := couchdb.FindRequest{
   236  		UseIndex: "by-source-and-type",
   237  		Selector: mango.And(
   238  			mango.Equal("type", permType),
   239  			mango.Equal("source_id", docType+"/"+slug),
   240  		),
   241  		Limit: 1,
   242  	}
   243  	err := couchdb.FindDocs(db, consts.Permissions, &req, &res)
   244  	if err != nil {
   245  		// With a cluster of couchdb, we can have a race condition where we
   246  		// query an index before it has been updated for an app that has
   247  		// just been created.
   248  		// Cf https://issues.apache.org/jira/browse/COUCHDB-3336
   249  		time.Sleep(1 * time.Second)
   250  		err = couchdb.FindDocs(db, consts.Permissions, &req, &res)
   251  		if err != nil {
   252  			return nil, err
   253  		}
   254  	}
   255  	if len(res) == 0 {
   256  		return nil, &couchdb.Error{
   257  			StatusCode: http.StatusNotFound,
   258  			Name:       "not_found",
   259  			Reason:     fmt.Sprintf("no permission doc for %v", slug),
   260  		}
   261  	}
   262  	perm := &res[0]
   263  	if perm.Expired() {
   264  		return nil, ErrExpiredToken
   265  	}
   266  	return perm, nil
   267  }
   268  
   269  // GetForShareCode retrieves the Permission doc for a given sharing code
   270  func GetForShareCode(db prefixer.Prefixer, tokenCode string) (*Permission, error) {
   271  	var res couchdb.ViewResponse
   272  	err := couchdb.ExecView(db, couchdb.PermissionsShareByCView, &couchdb.ViewRequest{
   273  		Key:         tokenCode,
   274  		IncludeDocs: true,
   275  	}, &res)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  
   280  	if len(res.Rows) == 0 {
   281  		msg := fmt.Sprintf("no permission doc for token %v", tokenCode)
   282  		return nil, echo.NewHTTPError(http.StatusForbidden, msg)
   283  	}
   284  
   285  	if len(res.Rows) > 1 {
   286  		msg := fmt.Sprintf("Bad state: several permission docs for token %v", tokenCode)
   287  		return nil, echo.NewHTTPError(http.StatusBadRequest, msg)
   288  	}
   289  
   290  	perm := &Permission{}
   291  	err = json.Unmarshal(res.Rows[0].Doc, perm)
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  
   296  	if perm.Expired() {
   297  		return nil, ErrExpiredToken
   298  	}
   299  
   300  	// Check for sharing made by a webapp/konnector that the app is still
   301  	// present (but not for OAuth). It is not checked in development release,
   302  	// since the --appdir does not create the expected document.
   303  	if !build.IsDevRelease() {
   304  		parts := strings.SplitN(perm.SourceID, "/", 2)
   305  		if len(parts) == 2 {
   306  			var doc couchdb.JSONDoc
   307  			docID := parts[0] + "/" + parts[1]
   308  			if parts[0] == consts.Sharings {
   309  				docID = parts[1]
   310  			}
   311  			if err := couchdb.GetDoc(db, parts[0], docID, &doc); err != nil {
   312  				return nil, ErrExpiredToken
   313  			}
   314  		}
   315  	}
   316  	return perm, nil
   317  }
   318  
   319  // GetTokenFromShortcode retrieves the token doc for a given sharing shortcode
   320  func GetTokenFromShortcode(db prefixer.Prefixer, shortcode string) (string, error) {
   321  	token, _, err := GetTokenAndPermissionsFromShortcode(db, shortcode)
   322  	return token, err
   323  }
   324  
   325  // GetTokenAndPermissionsFromShortcode retrieves the token and permissions doc for a given sharing shortcode
   326  func GetTokenAndPermissionsFromShortcode(db prefixer.Prefixer, shortcode string) (string, *Permission, error) {
   327  	var res couchdb.ViewResponse
   328  
   329  	err := couchdb.ExecView(db, couchdb.PermissionsShareByShortcodeView, &couchdb.ViewRequest{
   330  		Key:         shortcode,
   331  		IncludeDocs: true,
   332  	}, &res)
   333  	if err != nil {
   334  		return "", nil, err
   335  	}
   336  
   337  	if len(res.Rows) == 0 {
   338  		msg := fmt.Sprintf("no permission doc for shortcode %v", shortcode)
   339  		return "", nil, echo.NewHTTPError(http.StatusForbidden, msg)
   340  	}
   341  
   342  	if len(res.Rows) > 1 {
   343  		msg := fmt.Sprintf("Bad state: several permission docs for shortcode %v", shortcode)
   344  		return "", nil, echo.NewHTTPError(http.StatusBadRequest, msg)
   345  	}
   346  
   347  	perm := Permission{}
   348  	err = json.Unmarshal(res.Rows[0].Doc, &perm)
   349  
   350  	if err != nil {
   351  		return "", nil, err
   352  	}
   353  
   354  	for mail, code := range perm.Codes {
   355  		if mail == res.Rows[0].Value {
   356  			return code, &perm, nil
   357  		}
   358  	}
   359  
   360  	return "", nil, fmt.Errorf("Cannot find token for shortcode %s", res.Rows[0].Key)
   361  }
   362  
   363  // CreateWebappSet creates a Permission doc for an app
   364  func CreateWebappSet(db prefixer.Prefixer, slug string, set Set, version string) (*Permission, error) {
   365  	existing, _ := GetForWebapp(db, slug)
   366  	if existing != nil {
   367  		return nil, fmt.Errorf("There is already a permission doc for %v", slug)
   368  	}
   369  	// Add metadata
   370  	md, err := metadata.NewWithApp(slug, version, DocTypeVersion)
   371  	if err != nil {
   372  		return nil, err
   373  	}
   374  	return createAppSet(db, TypeWebapp, consts.Apps, slug, set, md)
   375  }
   376  
   377  // CreateKonnectorSet creates a Permission doc for a konnector
   378  func CreateKonnectorSet(db prefixer.Prefixer, slug string, set Set, version string) (*Permission, error) {
   379  	existing, _ := GetForKonnector(db, slug)
   380  	if existing != nil {
   381  		return nil, fmt.Errorf("There is already a permission doc for %v", slug)
   382  	}
   383  	// Add metadata
   384  	md, err := metadata.NewWithApp(slug, version, DocTypeVersion)
   385  	if err != nil {
   386  		return nil, err
   387  	}
   388  	return createAppSet(db, TypeKonnector, consts.Konnectors, slug, set, md)
   389  }
   390  
   391  func createAppSet(db prefixer.Prefixer, typ, docType, slug string, set Set, md *metadata.CozyMetadata) (*Permission, error) {
   392  	doc := &Permission{
   393  		Type:        typ,
   394  		SourceID:    docType + "/" + slug,
   395  		Permissions: set,
   396  		Metadata:    md,
   397  	}
   398  	err := couchdb.CreateDoc(db, doc)
   399  	if err != nil {
   400  		return nil, err
   401  	}
   402  	return doc, nil
   403  }
   404  
   405  // MergeExtraPermissions merges rules from "extraPermissions" set by adding them
   406  // in the "perms" one
   407  func MergeExtraPermissions(perms, extraPermissions Set) (Set, error) {
   408  	var permissions Set
   409  
   410  	// Appending the extraPermissions which are not in the target permissions
   411  	for _, ep := range extraPermissions {
   412  		found := false
   413  		for _, p := range perms {
   414  			if ep.Title == p.Title {
   415  				found = true
   416  				break
   417  			}
   418  		}
   419  		if !found {
   420  			permissions = append(permissions, ep)
   421  		}
   422  	}
   423  
   424  	// Merging the rules already existing
   425  	for _, rule := range perms {
   426  		found := false
   427  		for _, newRule := range extraPermissions {
   428  			if rule.Title == newRule.Title {
   429  				mergedRule, err := rule.Merge(newRule)
   430  				if err != nil {
   431  					continue
   432  				}
   433  				permissions = append(permissions, *mergedRule)
   434  				found = true
   435  				break
   436  			}
   437  		}
   438  		if !found {
   439  			permissions = append(permissions, rule)
   440  		}
   441  	}
   442  
   443  	return permissions, nil
   444  }
   445  
   446  // UpdateWebappSet creates a Permission doc for an app
   447  func UpdateWebappSet(db prefixer.Prefixer, slug string, set Set) (*Permission, error) {
   448  	doc, err := GetForWebapp(db, slug)
   449  	if err != nil {
   450  		return nil, err
   451  	}
   452  	return updateAppSet(db, doc, TypeWebapp, consts.Apps, slug, set)
   453  }
   454  
   455  // UpdateKonnectorSet creates a Permission doc for a konnector
   456  func UpdateKonnectorSet(db prefixer.Prefixer, slug string, set Set) (*Permission, error) {
   457  	doc, err := GetForKonnector(db, slug)
   458  	if err != nil {
   459  		return nil, err
   460  	}
   461  	return updateAppSet(db, doc, TypeKonnector, consts.Konnectors, slug, set)
   462  }
   463  
   464  func updateAppSet(db prefixer.Prefixer, doc *Permission, typ, docType, slug string, set Set) (*Permission, error) {
   465  	doc.Permissions = set
   466  	if doc.Metadata == nil {
   467  		doc.Metadata, _ = metadata.NewWithApp(slug, "", DocTypeVersion)
   468  	} else {
   469  		doc.Metadata.ChangeUpdatedAt()
   470  	}
   471  	err := couchdb.UpdateDoc(db, doc)
   472  	if err != nil {
   473  		return nil, err
   474  	}
   475  	return doc, nil
   476  }
   477  
   478  func checkSetPermissions(set Set, parent *Permission) error {
   479  	if parent.Type != TypeWebapp && parent.Type != TypeKonnector && parent.Type != TypeOauth {
   480  		return ErrOnlyAppCanCreateSubSet
   481  	}
   482  	if !set.IsSubSetOf(parent.Permissions) {
   483  		return ErrNotSubset
   484  	}
   485  	for _, rule := range set {
   486  		// XXX io.cozy.files is allowed and handled with specific code for sharings
   487  		if MatchType(rule, consts.Files) {
   488  			continue
   489  		}
   490  		if err := CheckWritable(rule.Type); err != nil {
   491  			return err
   492  		}
   493  	}
   494  	return nil
   495  }
   496  
   497  // CreateShareSet creates a Permission doc for sharing by link
   498  func CreateShareSet(db prefixer.Prefixer, parent *Permission, sourceID string, codes, shortcodes map[string]string, subdoc Permission, expiresAt *time.Time) (*Permission, error) {
   499  	set := subdoc.Permissions
   500  	if err := checkSetPermissions(set, parent); err != nil {
   501  		return nil, err
   502  	}
   503  	// SourceID stays the same, allow quick destruction of all children permissions
   504  	doc := &Permission{
   505  		Type:        TypeShareByLink,
   506  		SourceID:    sourceID,
   507  		Permissions: set,
   508  		Codes:       codes,
   509  		ShortCodes:  shortcodes,
   510  		ExpiresAt:   expiresAt,
   511  		Metadata:    subdoc.Metadata,
   512  	}
   513  
   514  	if pass, ok := subdoc.Password.(string); ok && len(pass) > 0 {
   515  		hash, err := crypto.GenerateFromPassphrase([]byte(pass))
   516  		if err != nil {
   517  			return nil, err
   518  		}
   519  		doc.Password = hash
   520  	}
   521  
   522  	err := couchdb.CreateDoc(db, doc)
   523  	if err != nil {
   524  		return nil, err
   525  	}
   526  
   527  	return doc, nil
   528  }
   529  
   530  // CreateSharePreviewSet creates a Permission doc for previewing a sharing
   531  func CreateSharePreviewSet(db prefixer.Prefixer, sharingID string, codes, shortcodes map[string]string, subdoc Permission) (*Permission, error) {
   532  	doc := &Permission{
   533  		Type:        TypeSharePreview,
   534  		Permissions: subdoc.Permissions,
   535  		Codes:       codes,
   536  		ShortCodes:  shortcodes,
   537  		SourceID:    consts.Sharings + "/" + sharingID,
   538  		Metadata:    subdoc.Metadata,
   539  	}
   540  	err := couchdb.CreateDoc(db, doc)
   541  	if err != nil {
   542  		return nil, err
   543  	}
   544  	return doc, nil
   545  }
   546  
   547  // CreateShareInteractSet creates a Permission doc for reading/writing a note
   548  // inside a sharing
   549  func CreateShareInteractSet(db prefixer.Prefixer, sharingID string, codes map[string]string, subdoc Permission) (*Permission, error) {
   550  	doc := &Permission{
   551  		Type:        TypeShareInteract,
   552  		Permissions: subdoc.Permissions,
   553  		Codes:       codes,
   554  		SourceID:    consts.Sharings + "/" + sharingID,
   555  		Metadata:    subdoc.Metadata,
   556  	}
   557  	err := couchdb.CreateDoc(db, doc)
   558  	if err != nil {
   559  		return nil, err
   560  	}
   561  	return doc, nil
   562  }
   563  
   564  // ForceWebapp creates or updates a Permission doc for a given webapp
   565  func ForceWebapp(db prefixer.Prefixer, slug string, set Set) error {
   566  	existing, _ := GetForWebapp(db, slug)
   567  	doc := &Permission{
   568  		Type:        TypeWebapp,
   569  		SourceID:    consts.Apps + "/" + slug,
   570  		Permissions: set,
   571  	}
   572  	if existing == nil {
   573  		return couchdb.CreateDoc(db, doc)
   574  	}
   575  
   576  	doc.SetID(existing.ID())
   577  	doc.SetRev(existing.Rev())
   578  	return couchdb.UpdateDoc(db, doc)
   579  }
   580  
   581  // ForceKonnector creates or updates a Permission doc for a given konnector
   582  func ForceKonnector(db prefixer.Prefixer, slug string, set Set) error {
   583  	existing, _ := GetForKonnector(db, slug)
   584  	doc := &Permission{
   585  		Type:        TypeKonnector,
   586  		SourceID:    consts.Konnectors + "/" + slug,
   587  		Permissions: set,
   588  	}
   589  	if existing == nil {
   590  		return couchdb.CreateDoc(db, doc)
   591  	}
   592  
   593  	doc.SetID(existing.ID())
   594  	doc.SetRev(existing.Rev())
   595  	return couchdb.UpdateDoc(db, doc)
   596  }
   597  
   598  // DestroyWebapp remove all Permission docs for a given app
   599  func DestroyWebapp(db prefixer.Prefixer, slug string) error {
   600  	return destroyApp(db, TypeWebapp, consts.Apps, slug)
   601  }
   602  
   603  // DestroyKonnector remove all Permission docs for a given konnector
   604  func DestroyKonnector(db prefixer.Prefixer, slug string) error {
   605  	return destroyApp(db, TypeKonnector, consts.Konnectors, slug)
   606  }
   607  
   608  func destroyApp(db prefixer.Prefixer, permType, docType, slug string) error {
   609  	var res []Permission
   610  	err := couchdb.FindDocs(db, consts.Permissions, &couchdb.FindRequest{
   611  		UseIndex: "by-source-and-type",
   612  		Selector: mango.And(
   613  			mango.Equal("source_id", docType+"/"+slug),
   614  			mango.Equal("type", permType),
   615  		),
   616  		Limit: 1000,
   617  	}, &res)
   618  	if err != nil {
   619  		return err
   620  	}
   621  	for _, p := range res {
   622  		err := couchdb.DeleteDoc(db, &p)
   623  		if err != nil {
   624  			return err
   625  		}
   626  	}
   627  	return nil
   628  }
   629  
   630  // GetPermissionsForIDs gets permissions for several IDs
   631  // returns for every id the combined allowed verbset
   632  func GetPermissionsForIDs(db prefixer.Prefixer, doctype string, ids []string) (map[string]*VerbSet, error) {
   633  	var res struct {
   634  		Rows []struct {
   635  			ID    string   `json:"id"`
   636  			Key   []string `json:"key"`
   637  			Value *VerbSet `json:"value"`
   638  		} `json:"rows"`
   639  	}
   640  
   641  	keys := make([]interface{}, len(ids))
   642  	for i, id := range ids {
   643  		keys[i] = []string{doctype, "_id", id}
   644  	}
   645  
   646  	err := couchdb.ExecView(db, couchdb.PermissionsShareByDocView, &couchdb.ViewRequest{
   647  		Keys: keys,
   648  	}, &res)
   649  	if err != nil {
   650  		return nil, err
   651  	}
   652  
   653  	result := make(map[string]*VerbSet)
   654  	for _, row := range res.Rows {
   655  		if _, ok := result[row.Key[2]]; ok {
   656  			result[row.Key[2]].Merge(row.Value)
   657  		} else {
   658  			result[row.Key[2]] = row.Value
   659  		}
   660  	}
   661  
   662  	return result, nil
   663  }
   664  
   665  // GetPermissionsByDoctype returns the list of all permissions of the given
   666  // type (shared-with-me by example) that have at least one rule for the given
   667  // doctype. The cursor will be modified in place.
   668  func GetPermissionsByDoctype(db prefixer.Prefixer, permType, doctype string, cursor couchdb.Cursor) ([]Permission, error) {
   669  	req := &couchdb.ViewRequest{
   670  		Key:         [2]interface{}{doctype, permType},
   671  		IncludeDocs: true,
   672  	}
   673  	cursor.ApplyTo(req)
   674  
   675  	var res couchdb.ViewResponse
   676  	err := couchdb.ExecView(db, couchdb.PermissionsByDoctype, req, &res)
   677  	if err != nil {
   678  		return nil, err
   679  	}
   680  	cursor.UpdateFrom(&res)
   681  
   682  	result := make([]Permission, len(res.Rows))
   683  
   684  	for i, row := range res.Rows {
   685  		var doc Permission
   686  		err := json.Unmarshal(row.Doc, &doc)
   687  		if err != nil {
   688  			return nil, err
   689  		}
   690  		result[i] = doc
   691  	}
   692  
   693  	return result, nil
   694  }