Skip to content

Commit 9620bb2

Browse files
Updates and Deletes protected
1 parent ee0bb3c commit 9620bb2

3 files changed

Lines changed: 304 additions & 0 deletions

File tree

edgraph/server.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"sort"
1717
"strconv"
1818
"strings"
19+
"sync"
1920
"sync/atomic"
2021
"time"
2122
"unicode"
@@ -573,6 +574,11 @@ func (s *Server) doMutate(ctx context.Context, qc *queryContext, resp *api.Respo
573574
if err != nil {
574575
return errors.Wrapf(err, "While doing mutations:")
575576
}
577+
578+
// Check program authorization for modifying existing data (after namespace is available)
579+
if err := checkMutationProgramAuth(ctx, edges, ns); err != nil {
580+
return err
581+
}
576582
predHints := make(map[string]pb.Metadata_HintType)
577583
for _, gmu := range qc.gmuList {
578584
for pred, hint := range gmu.Metadata.GetPredHints() {
@@ -660,6 +666,10 @@ func (s *Server) doMutate(ctx context.Context, qc *queryContext, resp *api.Respo
660666
resp.Txn.Keys = resp.Txn.Keys[:0]
661667
resp.Txn.CommitTs = cts
662668
calculateMutationMetrics()
669+
670+
// Update the program facet cache for future authorization checks
671+
updateProgramFacetCache(ns, edges)
672+
663673
return nil
664674
}
665675

@@ -2245,6 +2255,148 @@ func parseSubject(predSubject string) (uint64, error) {
22452255
}
22462256
}
22472257

2258+
// programFacetCache stores recently committed program facets for authorization checks.
2259+
//
2260+
// WHY THIS EXISTS:
2261+
// Dgraph's MemoryLayer is a read-through cache - it only populates on reads, not writes.
2262+
// When a mutation commits, the data goes to badger asynchronously. The MemoryLayer's
2263+
// updateItemInCache() only updates keys that are ALREADY cached; it doesn't add new ones.
2264+
// (See posting/mvcc.go:589-591)
2265+
//
2266+
// For program authorization, we need to check if a user can modify existing data BEFORE
2267+
// the Raft proposal. But for recently created data, the MemoryLayer won't have it cached
2268+
// and badger won't have synced it yet, so GetNoStoreSafe returns 0 postings.
2269+
//
2270+
// This cache provides write-through semantics specifically for program facets:
2271+
// - Updated after successful CommitOverNetwork in doMutate()
2272+
// - Checked in checkMutationProgramAuth() before the posting store fallback
2273+
//
2274+
// FUTURE CONSIDERATION:
2275+
// A more holistic approach might store program data directly in the Posting protobuf
2276+
// rather than as facets, which would integrate naturally with the existing caching.
2277+
// However, that would require protobuf changes and migration strategy.
2278+
var programFacetCache = struct {
2279+
sync.RWMutex
2280+
// Key: "namespace-predicate-entity", Value: comma-separated programs
2281+
data map[string]string
2282+
}{data: make(map[string]string)}
2283+
2284+
func programFacetCacheKey(ns uint64, attr string, entity uint64) string {
2285+
return fmt.Sprintf("%d-%s-%d", ns, attr, entity)
2286+
}
2287+
2288+
// updateProgramFacetCache updates the cache with program facets from committed mutations.
2289+
func updateProgramFacetCache(ns uint64, edges []*pb.DirectedEdge) {
2290+
programFacetCache.Lock()
2291+
defer programFacetCache.Unlock()
2292+
2293+
for _, edge := range edges {
2294+
if edge.Entity == 0 {
2295+
continue
2296+
}
2297+
for _, f := range edge.Facets {
2298+
if f.Key == x.ProgramFacetKey {
2299+
key := programFacetCacheKey(ns, edge.Attr, edge.Entity)
2300+
programFacetCache.data[key] = string(f.Value)
2301+
glog.V(1).Infof("updateProgramFacetCache: cached programs=%s for key=%s", f.Value, key)
2302+
break
2303+
}
2304+
}
2305+
}
2306+
}
2307+
2308+
// checkMutationProgramAuth verifies that the user is authorized to modify existing data
2309+
// protected by program facets. This check happens before Raft proposal.
2310+
func checkMutationProgramAuth(ctx context.Context, edges []*pb.DirectedEdge, ns uint64) error {
2311+
authCtx := auth.ExtractOrNil(ctx)
2312+
if authCtx == nil || authCtx.IsNil {
2313+
// No auth context means no program restrictions - allow mutation
2314+
glog.V(1).Infof("checkMutationProgramAuth: no auth context, allowing mutation")
2315+
return nil
2316+
}
2317+
2318+
glog.V(1).Infof("checkMutationProgramAuth: auth context has programs=%v", authCtx.Programs)
2319+
2320+
// Build a set of user's programs for fast lookup
2321+
userPrograms := make(map[string]bool)
2322+
for _, p := range authCtx.Programs {
2323+
userPrograms[p] = true
2324+
}
2325+
2326+
for _, edge := range edges {
2327+
// Only check existing entities (non-zero UID)
2328+
if edge.Entity == 0 {
2329+
continue
2330+
}
2331+
2332+
// First check the in-memory cache for recently committed program facets
2333+
cacheKey := programFacetCacheKey(ns, edge.Attr, edge.Entity)
2334+
programFacetCache.RLock()
2335+
cachedPrograms, found := programFacetCache.data[cacheKey]
2336+
programFacetCache.RUnlock()
2337+
2338+
if found {
2339+
glog.V(1).Infof("checkMutationProgramAuth: found cached programs=%s for Entity=%d, Attr=%s",
2340+
cachedPrograms, edge.Entity, edge.Attr)
2341+
programs := strings.Split(cachedPrograms, ",")
2342+
hasMatch := false
2343+
for _, prog := range programs {
2344+
if userPrograms[prog] {
2345+
hasMatch = true
2346+
break
2347+
}
2348+
}
2349+
if !hasMatch && len(programs) > 0 && programs[0] != "" {
2350+
glog.V(1).Infof("checkMutationProgramAuth: DENYING - no program match (from cache)")
2351+
return errors.Errorf("not authorized to modify program-protected data")
2352+
}
2353+
continue // Authorized via cache, move to next edge
2354+
}
2355+
2356+
// Fall back to reading from posting store for older data
2357+
namespacedAttr := x.NamespaceAttr(ns, edge.Attr)
2358+
key := x.DataKey(namespacedAttr, edge.Entity)
2359+
glog.V(1).Infof("checkMutationProgramAuth: checking store for Entity=%d, Attr=%s", edge.Entity, edge.Attr)
2360+
2361+
pl, err := posting.GetNoStoreSafe(key, math.MaxUint64)
2362+
if err != nil || pl == nil {
2363+
continue
2364+
}
2365+
2366+
// Check each existing posting for program authorization
2367+
var authErr error
2368+
pl.Iterate(math.MaxUint64, 0, func(p *pb.Posting) error {
2369+
for _, f := range p.Facets {
2370+
if f.Key == x.ProgramFacetKey {
2371+
programs := strings.Split(string(f.Value), ",")
2372+
glog.V(1).Infof("checkMutationProgramAuth: store has programs=%v, user has=%v", programs, authCtx.Programs)
2373+
hasMatch := false
2374+
for _, prog := range programs {
2375+
if userPrograms[prog] {
2376+
hasMatch = true
2377+
break
2378+
}
2379+
}
2380+
if !hasMatch && len(programs) > 0 && programs[0] != "" {
2381+
glog.V(1).Infof("checkMutationProgramAuth: DENYING - no program match (from store)")
2382+
authErr = errors.Errorf("not authorized to modify program-protected data")
2383+
return posting.ErrStopIteration
2384+
}
2385+
break
2386+
}
2387+
}
2388+
return nil
2389+
})
2390+
2391+
if authErr != nil {
2392+
return authErr
2393+
}
2394+
}
2395+
2396+
glog.V(1).Infof("checkMutationProgramAuth: allowing mutation")
2397+
return nil
2398+
}
2399+
22482400
// injectProgramFacets adds the dgraph.programs facet to edges based on the auth context.
22492401
// If the user has programs in their auth context, those programs are attached to each edge
22502402
// being mutated. This enables server-side filtering during queries.

posting/lists.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ func GetNoStore(key []byte, readTs uint64) (rlist *List, err error) {
5757
return getNew(key, pstore, readTs, false)
5858
}
5959

60+
// GetNoStoreSafe is like GetNoStore but returns nil if the store is not initialized.
61+
// This is safe to call even before the posting package is fully initialized.
62+
func GetNoStoreSafe(key []byte, readTs uint64) (rlist *List, err error) {
63+
if pstore == nil {
64+
return nil, nil
65+
}
66+
return getNew(key, pstore, readTs, false)
67+
}
68+
6069
// LocalCache stores a cache of posting lists and deltas.
6170
// This doesn't sync, so call this only when you don't care about dirty posting lists in
6271
// memory(for example before populating snapshot) or after calling syncAllMarks

systest/label/label_auth_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,149 @@ func TestProgramAuthUidTraversal(t *testing.T) {
833833
t.Log("Test passed: UID traversal respects program auth")
834834
}
835835

836+
// TestProgramAuthUpdateBlocked tests that users cannot update program-protected data they don't have access to.
837+
func TestProgramAuthUpdateBlocked(t *testing.T) {
838+
t.Log("=== TestProgramAuthUpdateBlocked: Users cannot update data they can't access ===")
839+
setupProgramAuthTest(t)
840+
dg := waitForCluster(t)
841+
842+
// Insert data with ALPHA program
843+
ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{
844+
Programs: []string{"ALPHA"},
845+
})
846+
resp, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{
847+
CommitNow: true,
848+
SetNquads: []byte(`_:p1 <name> "Alpha Secret" .`),
849+
})
850+
require.NoError(t, err)
851+
uid := resp.Uids["p1"]
852+
t.Logf("Created ALPHA-protected node with UID: %s", uid)
853+
854+
// Try to update with BRAVO program - should fail
855+
ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{
856+
Programs: []string{"BRAVO"},
857+
})
858+
t.Log("Attempting to update ALPHA data with BRAVO credentials...")
859+
_, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{
860+
CommitNow: true,
861+
SetNquads: []byte(fmt.Sprintf(`<%s> <name> "Hacked by BRAVO" .`, uid)),
862+
})
863+
require.Error(t, err, "BRAVO user should not be able to update ALPHA data")
864+
require.Contains(t, err.Error(), "not authorized", "error should indicate authorization failure")
865+
t.Log("Test passed: update correctly blocked")
866+
}
867+
868+
// TestProgramAuthDeleteBlocked tests that users cannot delete program-protected data they don't have access to.
869+
func TestProgramAuthDeleteBlocked(t *testing.T) {
870+
t.Log("=== TestProgramAuthDeleteBlocked: Users cannot delete data they can't access ===")
871+
setupProgramAuthTest(t)
872+
dg := waitForCluster(t)
873+
874+
// Insert data with ALPHA program
875+
ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{
876+
Programs: []string{"ALPHA"},
877+
})
878+
resp, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{
879+
CommitNow: true,
880+
SetNquads: []byte(`_:p1 <name> "Alpha Secret" .`),
881+
})
882+
require.NoError(t, err)
883+
uid := resp.Uids["p1"]
884+
t.Logf("Created ALPHA-protected node with UID: %s", uid)
885+
886+
// Try to delete with BRAVO program - should fail
887+
ctxBravo := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{
888+
Programs: []string{"BRAVO"},
889+
})
890+
t.Log("Attempting to delete ALPHA data with BRAVO credentials...")
891+
_, err = dg.NewTxn().Mutate(ctxBravo, &api.Mutation{
892+
CommitNow: true,
893+
DelNquads: []byte(fmt.Sprintf(`<%s> <name> * .`, uid)),
894+
})
895+
require.Error(t, err, "BRAVO user should not be able to delete ALPHA data")
896+
require.Contains(t, err.Error(), "not authorized", "error should indicate authorization failure")
897+
898+
// Verify data still exists with ALPHA credentials
899+
queryResp, err := dg.NewTxn().Query(ctxAlpha, fmt.Sprintf(`
900+
{
901+
node(func: uid(%s)) {
902+
name
903+
}
904+
}
905+
`, uid))
906+
require.NoError(t, err)
907+
require.Contains(t, string(queryResp.GetJson()), "Alpha Secret", "data should still exist")
908+
t.Log("Test passed: delete correctly blocked")
909+
}
910+
911+
// TestProgramAuthUpdateAllowed tests that authorized users can update their own data.
912+
func TestProgramAuthUpdateAllowed(t *testing.T) {
913+
t.Log("=== TestProgramAuthUpdateAllowed: Users can update data they have access to ===")
914+
setupProgramAuthTest(t)
915+
dg := waitForCluster(t)
916+
917+
// Insert data with ALPHA program
918+
ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{
919+
Programs: []string{"ALPHA"},
920+
})
921+
resp, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{
922+
CommitNow: true,
923+
SetNquads: []byte(`_:p1 <name> "Original Name" .`),
924+
})
925+
require.NoError(t, err)
926+
uid := resp.Uids["p1"]
927+
928+
// Update with same ALPHA program - should succeed
929+
t.Log("Updating with ALPHA credentials...")
930+
_, err = dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{
931+
CommitNow: true,
932+
SetNquads: []byte(fmt.Sprintf(`<%s> <name> "Updated Name" .`, uid)),
933+
})
934+
require.NoError(t, err, "ALPHA user should be able to update ALPHA data")
935+
936+
// Verify update
937+
queryResp, err := dg.NewTxn().Query(ctxAlpha, fmt.Sprintf(`
938+
{
939+
node(func: uid(%s)) {
940+
name
941+
}
942+
}
943+
`, uid))
944+
require.NoError(t, err)
945+
require.Contains(t, string(queryResp.GetJson()), "Updated Name")
946+
t.Log("Test passed: authorized update succeeded")
947+
}
948+
949+
// TestProgramAuthNoAuthCannotModifyProtected tests that users without auth cannot modify protected data.
950+
func TestProgramAuthNoAuthCannotModifyProtected(t *testing.T) {
951+
t.Log("=== TestProgramAuthNoAuthCannotModifyProtected: No-auth users cannot modify protected data ===")
952+
setupProgramAuthTest(t)
953+
dg := waitForCluster(t)
954+
955+
// Insert data with ALPHA program
956+
ctxAlpha := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{
957+
Programs: []string{"ALPHA"},
958+
})
959+
resp, err := dg.NewTxn().Mutate(ctxAlpha, &api.Mutation{
960+
CommitNow: true,
961+
SetNquads: []byte(`_:p1 <name> "Protected Data" .`),
962+
})
963+
require.NoError(t, err)
964+
uid := resp.Uids["p1"]
965+
966+
// Try to update with empty auth context (auth active but no programs)
967+
ctxEmpty := auth.AttachToOutgoingContext(context.Background(), &auth.AuthContext{
968+
Programs: []string{},
969+
})
970+
t.Log("Attempting to update protected data with empty auth...")
971+
_, err = dg.NewTxn().Mutate(ctxEmpty, &api.Mutation{
972+
CommitNow: true,
973+
SetNquads: []byte(fmt.Sprintf(`<%s> <name> "Attempted Overwrite" .`, uid)),
974+
})
975+
require.Error(t, err, "user with no programs should not be able to modify protected data")
976+
t.Log("Test passed: empty auth cannot modify protected data")
977+
}
978+
836979
// TestProgramAuthComparisonOps tests program auth with comparison operators (lt, gt, le, ge).
837980
func TestProgramAuthComparisonOps(t *testing.T) {
838981
t.Log("=== TestProgramAuthComparisonOps: comparison operators should respect program auth ===")

0 commit comments

Comments
 (0)