package influxdb import ( "context" ) // ErrDocumentNotFound is the error msg for a missing document. const ErrDocumentNotFound = "document not found" // DocumentService is used to create/find instances of document stores. type DocumentService interface { CreateDocumentStore(ctx context.Context, name string) (DocumentStore, error) FindDocumentStore(ctx context.Context, name string) (DocumentStore, error) } // Document is a generic structure for stating data. type Document struct { ID ID `json:"id"` Meta DocumentMeta `json:"meta"` Content interface{} `json:"content,omitempty"` // TODO(desa): maybe this needs to be json.Marshaller & json.Unmarshaler Labels []*Label `json:"labels,omitempty"` // read only } // DocumentMeta is information that is universal across documents. Ideally // data in the meta should be indexed and queryable. type DocumentMeta struct { Name string `json:"name"` Version string `json:"version,omitempty"` } // DocumentStore is used to perform CRUD operations on documents. It follows an options // pattern that allows users to perform actions related to documents in a transactional way. type DocumentStore interface { CreateDocument(ctx context.Context, d *Document, opts ...DocumentOptions) error UpdateDocument(ctx context.Context, d *Document, opts ...DocumentOptions) error FindDocuments(ctx context.Context, opts ...DocumentFindOptions) ([]*Document, error) DeleteDocuments(ctx context.Context, opts ...DocumentFindOptions) error } // DocumentIndex is a structure that is used in DocumentOptions to perform operations // related to labels and ownership. type DocumentIndex interface { // TODO(desa): support users as document owners eventually AddDocumentOwner(docID ID, ownerType string, ownerID ID) error RemoveDocumentOwner(docID ID, ownerType string, ownerID ID) error GetAccessorsDocuments(ownerType string, ownerID ID) ([]ID, error) GetDocumentsAccessors(docID ID) ([]ID, error) UsersOrgs(userID ID) ([]ID, error) // IsOrgAccessor checks to see if the userID provided is allowed to access // the orgID privided. If the lookup is done in a writable operation // then this method should ensure that the user is an org owner. If the // operation is readable, then it should only require that the user is an org // member. IsOrgAccessor(userID, orgID ID) error FindOrganizationByName(n string) (ID, error) FindLabelByName(n string) (ID, error) AddDocumentLabel(docID, labelID ID) error RemoveDocumentLabel(docID, labelID ID) error // TODO(desa): support finding document by label } // DocumentDecorator passes information to the DocumentStore about the presentation // of the data being retrieved. It can be used to include the content or the labels // associated with a document. type DocumentDecorator interface { IncludeContent() error IncludeLabels() error // TODO(desa): add support for including owners. } // IncludeContent signals to the DocumentStore that the content of the document // should be included. func IncludeContent(_ DocumentIndex, dd DocumentDecorator) ([]ID, error) { return nil, dd.IncludeContent() } // IncludeLabels signals to the DocumentStore that the documents labels // should be included. func IncludeLabels(_ DocumentIndex, dd DocumentDecorator) ([]ID, error) { return nil, dd.IncludeLabels() } // DocumentOptions are specified during create/update. They can be used to add labels/owners // to documents. During Create, options are executed after the creation of the document has // taken place. During Update, they happen before. type DocumentOptions func(ID, DocumentIndex) error // DocumentFindOptions are speficied during find/delete. They are used to lookup // documents using labels/owners. // TODO(desa): consider changing this to have a single struct that has both // the decorator and the index on it. type DocumentFindOptions func(DocumentIndex, DocumentDecorator) ([]ID, error) // WithOrg adds the provided org as an owner of the document. func WithOrg(org string) func(ID, DocumentIndex) error { return func(id ID, idx DocumentIndex) error { oid, err := idx.FindOrganizationByName(org) if err != nil { return err } return idx.AddDocumentOwner(id, "org", oid) } } // AuthorizedWithOrg adds the provided org as an owner of the document if // the authorizer is allowed to access the org in being added. func AuthorizedWithOrg(a Authorizer, org string) func(ID, DocumentIndex) error { switch t := a.(type) { case *Authorization: return TokenAuthorizedWithOrg(t, org) } return func(id ID, idx DocumentIndex) error { oid, err := idx.FindOrganizationByName(org) if err != nil { return err } if err := idx.IsOrgAccessor(a.GetUserID(), oid); err != nil { return err } return idx.AddDocumentOwner(id, "org", oid) } } // TokenAuthorizedWithOrg ensures that the authorization provided is allowed to perform // write actions against the org provided. func TokenAuthorizedWithOrg(a *Authorization, org string) func(ID, DocumentIndex) error { return func(id ID, idx DocumentIndex) error { if !a.IsActive() { return &Error{ Code: EUnauthorized, Msg: "authorization cannot add org as document owner", } } oid, err := idx.FindOrganizationByName(org) if err != nil { return err } p := Permission{ Action: WriteAction, Resource: Resource{ Type: DocumentsResourceType, OrgID: &oid, }, } if !a.Allowed(p) { return &Error{ Code: EUnauthorized, Msg: "authorization cannot add org as document owner", } } return idx.AddDocumentOwner(id, "org", oid) } } // WithOrgID adds the provided org as an owner of the document. func WithOrgID(orgID ID) func(ID, DocumentIndex) error { return func(id ID, idx DocumentIndex) error { return idx.AddDocumentOwner(id, "org", orgID) } } // AuthorizedWithOrgID adds the provided org as an owner of the document if // the authorizer is allowed to access the org in being added. func AuthorizedWithOrgID(a Authorizer, orgID ID) func(ID, DocumentIndex) error { switch t := a.(type) { case *Authorization: return TokenAuthorizedWithOrgID(t, orgID) } return func(id ID, idx DocumentIndex) error { if err := idx.IsOrgAccessor(a.GetUserID(), orgID); err != nil { return err } return idx.AddDocumentOwner(id, "org", orgID) } } // TokenAuthorizedWithOrgID ensures that the authorization provided is allowed to perform // write actions against the org provided. func TokenAuthorizedWithOrgID(a *Authorization, orgID ID) func(ID, DocumentIndex) error { return func(id ID, idx DocumentIndex) error { if !a.IsActive() { return &Error{ Code: EUnauthorized, Msg: "authorization cannot add org as document owner", } } p := Permission{ Action: WriteAction, Resource: Resource{ Type: DocumentsResourceType, OrgID: &orgID, }, } if !a.Allowed(p) { return &Error{ Code: EUnauthorized, Msg: "authorization cannot add org as document owner", } } return idx.AddDocumentOwner(id, "org", orgID) } } // WithLabel adds a label to the documents where it is applied. func WithLabel(label string) func(ID, DocumentIndex) error { return func(id ID, idx DocumentIndex) error { // TODO(desa): turns out that labels are application global, at somepoint we'll // want to scope these by org. We should add auth at that point. lid, err := idx.FindLabelByName(label) if err != nil { return err } return idx.AddDocumentLabel(id, lid) } } // WithoutLabel removes a label to the documents where it is applied. func WithoutLabel(label string) func(ID, DocumentIndex) error { return func(id ID, idx DocumentIndex) error { // TODO(desa): turns out that labels are application global, at somepoint we'll // want to scope these by org. We should add auth at that point. lid, err := idx.FindLabelByName(label) if err != nil { return err } return idx.RemoveDocumentLabel(id, lid) } } // Authorized checks to see if the user is authorized to access the document provided. // If the authorizer is a token, then it checks the tokens permissions. Otherwise, // it checks to see if the user associated with the authorizer is an accessor // of the of the org that owns the document. func Authorized(a Authorizer) func(ID, DocumentIndex) error { switch t := a.(type) { case *Authorization: return TokenAuthorized(t) } return func(docID ID, idx DocumentIndex) error { oids, err := idx.GetDocumentsAccessors(docID) if err != nil { return err } for _, oid := range oids { if err := idx.IsOrgAccessor(a.GetUserID(), oid); err == nil { return nil } } return &Error{ Code: EUnauthorized, Msg: "authorizer cannot access document", } } } // TokenAuthorized checks to see if the authorization provided is allowed access the orgs documents. func TokenAuthorized(a *Authorization) func(ID, DocumentIndex) error { return func(id ID, idx DocumentIndex) error { if !a.IsActive() { return &Error{ Code: EUnauthorized, Msg: "authorizer cannot access document", } } oids, err := idx.GetDocumentsAccessors(id) if err != nil { return err } orgs := map[ID]bool{} for _, oid := range oids { orgs[oid] = true } for _, p := range a.Permissions { if p.Action == ReadAction { continue } // If the authz has a direct permission to access the resource if p.Resource.Type == DocumentsResourceType && p.Resource.ID != nil && *p.Resource.ID == id { return nil } // If the authz has a direct permission to access the class of resources if p.Resource.Type == DocumentsResourceType && p.Resource.ID == nil && p.Resource.OrgID == nil { return nil } if p.Resource.Type == DocumentsResourceType && p.Resource.OrgID != nil && orgs[*p.Resource.OrgID] { return nil } } return &Error{ Code: EUnauthorized, Msg: "authorization cannot access document", } } } // WhereOrg retrieves a list of the ids of the documents that belong to the provided org. func WhereOrg(org string) func(DocumentIndex, DocumentDecorator) ([]ID, error) { return func(idx DocumentIndex, _ DocumentDecorator) ([]ID, error) { oid, err := idx.FindOrganizationByName(org) if err != nil { return nil, err } return idx.GetAccessorsDocuments("org", oid) } } // AuthorizedWhereOrg ensures that the authorizer is allowed to access the org provideds documents and then // retrieves a list of the ids of the documents that belong to the provided org. func AuthorizedWhereOrg(a Authorizer, org string) func(DocumentIndex, DocumentDecorator) ([]ID, error) { switch t := a.(type) { case *Authorization: return TokenAuthorizedWhereOrg(t, org) } return func(idx DocumentIndex, _ DocumentDecorator) ([]ID, error) { oid, err := idx.FindOrganizationByName(org) if err != nil { return nil, err } if err := idx.IsOrgAccessor(a.GetUserID(), oid); err != nil { return nil, err } return idx.GetAccessorsDocuments("org", oid) } } // TokenAuthorizedWhereOrg ensures that the authorization is allowed to access the org provideds documents and then // retrieves a list of the ids of the documents that belong to the provided org. func TokenAuthorizedWhereOrg(a *Authorization, org string) func(DocumentIndex, DocumentDecorator) ([]ID, error) { return func(idx DocumentIndex, _ DocumentDecorator) ([]ID, error) { oid, err := idx.FindOrganizationByName(org) if err != nil { return nil, err } p := Permission{ // TODO(desa): this should be configurable, but should be sufficient for now. In particular this // means that tokens cannot be used to delete documents for now if the AuthorizedWhereOrg is called. Action: ReadAction, Resource: Resource{ Type: OrgsResourceType, OrgID: &oid, }, } if !a.Allowed(p) { return nil, &Error{ Code: EUnauthorized, Msg: "authorizer cannot access documents", } } return idx.GetAccessorsDocuments("org", oid) } } // AuthorizedWhere retrieves all documents that the user is authorized to access. This is done by // retrieving the list of all orgs where the user is an accessor and the retrieving all of their // documents. func AuthorizedWhere(a Authorizer) func(DocumentIndex, DocumentDecorator) ([]ID, error) { switch t := a.(type) { case *Authorization: return TokenAuthorizedWhere(t) } var ids []ID return func(idx DocumentIndex, _ DocumentDecorator) ([]ID, error) { dids, err := idx.GetAccessorsDocuments("user", a.GetUserID()) if err != nil { return nil, err } ids = append(ids, dids...) orgIDs, err := idx.UsersOrgs(a.GetUserID()) if err != nil { return nil, err } for _, orgID := range orgIDs { dids, err := idx.GetAccessorsDocuments("org", orgID) if err != nil { return nil, err } ids = append(ids, dids...) } // TODO(desa): we should probably dedupe this list eventually. return ids, nil } } // AuthorizedWhere retrieves all documents that the authorization is allowed to access. func TokenAuthorizedWhere(a *Authorization) func(DocumentIndex, DocumentDecorator) ([]ID, error) { // TODO(desa): what to do about retrieving all documents using auth? (e.g. write/read:documents/*) var ids []ID return func(idx DocumentIndex, _ DocumentDecorator) ([]ID, error) { if !a.IsActive() { return nil, &Error{ Code: EUnauthorized, Msg: "authorizer cannot access documents", } } for _, p := range a.Permissions { if p.Resource.Type == DocumentsResourceType && p.Resource.OrgID != nil { oids, err := idx.GetAccessorsDocuments("org", *p.Resource.OrgID) if err != nil { return nil, err } ids = append(ids, oids...) } if p.Resource.Type == DocumentsResourceType && p.Resource.ID != nil { ids = append(ids, *p.Resource.ID) } } return ids, nil } } // WhereID passes through the id provided. func WhereID(docID ID) func(DocumentIndex, DocumentDecorator) ([]ID, error) { return func(idx DocumentIndex, _ DocumentDecorator) ([]ID, error) { return []ID{docID}, nil } } // AuthorizedWhereID ensures that the authorizer provided either has the permission to access the document // or the user associated with the authorizer is an org accessor. func AuthorizedWhereID(a Authorizer, docID ID) func(DocumentIndex, DocumentDecorator) ([]ID, error) { switch t := a.(type) { case *Authorization: return TokenAuthorizedWhereID(t, docID) } return func(idx DocumentIndex, _ DocumentDecorator) ([]ID, error) { oids, err := idx.GetDocumentsAccessors(docID) if err != nil { return nil, err } for _, oid := range oids { if err := idx.IsOrgAccessor(a.GetUserID(), oid); err == nil { return []ID{docID}, nil } } return nil, &Error{ Code: EUnauthorized, Msg: "authorizer cannot access document", } } } // TokenAuthorizedWhereID ensures that the authorization provided has the permission to access the document. func TokenAuthorizedWhereID(a *Authorization, docID ID) func(DocumentIndex, DocumentDecorator) ([]ID, error) { return func(idx DocumentIndex, _ DocumentDecorator) ([]ID, error) { if !a.IsActive() { return nil, &Error{ Code: EUnauthorized, Msg: "authorizer cannot access documents", } } oids, err := idx.GetDocumentsAccessors(docID) if err != nil { return nil, err } orgs := map[ID]bool{} for _, oid := range oids { orgs[oid] = true } for _, p := range a.Permissions { // If the authz has a direct permission to access the resource if p.Resource.Type == DocumentsResourceType && p.Resource.ID != nil && docID == *p.Resource.ID { return []ID{docID}, nil } // If the authz has a direct permission to access the class of resources if p.Resource.Type == DocumentsResourceType && p.Resource.ID == nil && p.Resource.OrgID == nil { return []ID{docID}, nil } if p.Resource.Type == DocumentsResourceType && p.Resource.OrgID != nil && orgs[*p.Resource.OrgID] { return []ID{docID}, nil } } return nil, &Error{ Code: EUnauthorized, Msg: "authorization cannot access document", } } }