119 lines
4.1 KiB
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}
|
|
}
|
|
}
|