memos/server/access/memo.go

119 lines
4.1 KiB
Go

// Package access defines transport-independent resource authorization policies
// shared by the server's API and HTTP adapters.
package access
import "github.com/usememos/memos/store"
// MemoReadDenial describes why a memo read was rejected.
type MemoReadDenial int
const (
// MemoReadDenialNone means the read is allowed.
MemoReadDenialNone MemoReadDenial = iota
// MemoReadDenialNotFound hides missing, archived, and invalid memo state.
MemoReadDenialNotFound
// MemoReadDenialUnauthenticated means the resource requires a signed-in user.
MemoReadDenialUnauthenticated
// MemoReadDenialPermission means the signed-in user cannot read the resource.
MemoReadDenialPermission
)
// MemoReadClass describes whether the resource is anonymously readable.
type MemoReadClass int
const (
// MemoReadClassPrivate is for author, authenticated, member, or share-token reads.
MemoReadClassPrivate MemoReadClass = iota
// MemoReadClassPublic is for resources currently readable without credentials.
MemoReadClassPublic
)
// MemoReadDecision is the outcome of evaluating memo read access.
type MemoReadDecision struct {
Denial MemoReadDenial
Class MemoReadClass
}
// MemoReadContext contains the fully resolved authorization context for one
// memo. Relations never contribute authorization; callers evaluate each
// relation endpoint independently.
type MemoReadContext struct {
Memo *store.Memo
Viewer *store.User
AllowAnonymous bool
SharedMemoID *int32
CreatorValid bool
SpaceValid bool
ViewerSpaceMember bool
}
// Allowed reports whether the read is permitted.
func (d MemoReadDecision) Allowed() bool {
return d.Denial == MemoReadDenialNone
}
// CheckMemoReadContext evaluates access to exactly one memo. Unknown audience,
// invalid lifecycle state, and a missing or invalid creator fail closed. A
// dangling placement only invalidates SPACE reads; other audiences
// remain memo-local. A share applies only to the exact memo and never to either
// endpoint of a relation.
func CheckMemoReadContext(ctx MemoReadContext) MemoReadDecision {
memo := ctx.Memo
if memo == nil || !ctx.CreatorValid {
return MemoReadDecision{Denial: MemoReadDenialNotFound}
}
if memo.Visibility != store.Public && memo.Visibility != store.Protected && memo.Visibility != store.Private && memo.Visibility != store.SpaceAudience {
return MemoReadDecision{Denial: MemoReadDenialNotFound}
}
if memo.Visibility == store.SpaceAudience && (memo.SpaceID == nil || !ctx.SpaceValid) {
return MemoReadDecision{Denial: MemoReadDenialNotFound}
}
viewerActive := ctx.Viewer != nil && ctx.Viewer.RowStatus == store.Normal
viewerIsAuthor := viewerActive && ctx.Viewer.ID == memo.CreatorID
if memo.RowStatus == store.Archived && !viewerIsAuthor {
return MemoReadDecision{Denial: MemoReadDenialNotFound}
}
if memo.RowStatus != store.Normal && memo.RowStatus != store.Archived {
return MemoReadDecision{Denial: MemoReadDenialNotFound}
}
shareApplies := ctx.SharedMemoID != nil && memo.ID == *ctx.SharedMemoID && memo.Visibility != store.SpaceAudience
if shareApplies {
return MemoReadDecision{Class: MemoReadClassPrivate}
}
switch memo.Visibility {
case store.Public:
if ctx.AllowAnonymous {
return MemoReadDecision{Class: MemoReadClassPublic}
}
if viewerActive {
return MemoReadDecision{Class: MemoReadClassPrivate}
}
return MemoReadDecision{Denial: MemoReadDenialUnauthenticated}
case store.Protected:
if viewerActive {
return MemoReadDecision{Class: MemoReadClassPrivate}
}
return MemoReadDecision{Denial: MemoReadDenialUnauthenticated}
case store.Private:
if !viewerActive {
return MemoReadDecision{Denial: MemoReadDenialUnauthenticated}
}
if !viewerIsAuthor {
return MemoReadDecision{Denial: MemoReadDenialPermission}
}
return MemoReadDecision{Class: MemoReadClassPrivate}
case store.SpaceAudience:
if !viewerActive {
return MemoReadDecision{Denial: MemoReadDenialUnauthenticated}
}
if ctx.ViewerSpaceMember {
return MemoReadDecision{Class: MemoReadClassPrivate}
}
return MemoReadDecision{Denial: MemoReadDenialPermission}
default:
return MemoReadDecision{Denial: MemoReadDenialNotFound}
}
}