influxdb/document.go

546 lines
16 KiB
Go

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"`
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
Version string `json:"version,omitempty"`
CRUDLog
}
// 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)
FindOrganizationByID(id ID) error
FindLabelByID(id 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(lid ID) 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.
err := idx.FindLabelByID(lid)
if err != nil {
return err
}
return idx.AddDocumentLabel(id, lid)
}
}
// WithoutLabel removes a label to the documents where it is applied.
func WithoutLabel(lid ID) 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.
err := idx.FindLabelByID(lid)
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)
}
}
// AuthorizedWhereOrgID 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 orgID.
func AuthorizedWhereOrgID(a Authorizer, id ID) func(DocumentIndex, DocumentDecorator) ([]ID, error) {
return func(idx DocumentIndex, _ DocumentDecorator) ([]ID, error) {
if err := idx.FindOrganizationByID(id); err != nil {
return nil, err
}
return authorizedWhereOrgID(a, id, idx)
}
}
func authorizedWhereOrgID(a Authorizer, id ID, idx DocumentIndex) ([]ID, error) {
var isTokenAuth bool
switch a.(type) {
case *Authorization:
isTokenAuth = true
}
if isTokenAuth {
if !a.Allowed(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: &id,
},
}) {
return nil, &Error{
Code: EUnauthorized,
Msg: "authorizer cannot access documents",
}
}
} else {
if err := idx.IsOrgAccessor(a.GetUserID(), id); err != nil {
return nil, err
}
}
return idx.GetAccessorsDocuments("org", id)
}
// 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) {
return func(idx DocumentIndex, _ DocumentDecorator) ([]ID, error) {
oid, err := idx.FindOrganizationByName(org)
if err != nil {
return nil, err
}
return authorizedWhereOrgID(a, oid, idx)
}
}
// 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
}
return authorizedWhereOrgID(a, oid, idx)
}
}
// 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
}
}
// TokenAuthorizedWhere 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",
}
}
}