CoolFace
Apppublic

llzai/axonhub

sourceHugging Faceupdated 8mo agoView on Hugging Face
0likes
rule_user_project_scope.go182 linesDownload Raw Back to scopes
1package scopes
2
3import (
4	"context"
5
6	"entgo.io/ent/dialect/sql"
7	"entgo.io/ent/entql"
8	"github.com/samber/lo"
9
10	"github.com/looplj/axonhub/internal/contexts"
11	"github.com/looplj/axonhub/internal/ent"
12	"github.com/looplj/axonhub/internal/ent/privacy"
13)
14
15type ProjectOwnedFilter interface {
16	WhereProjectID(entql.IntP)
17}
18
19// userHasProjectScope checks if a user has the required scope for a specific project.
20func userHasProjectScope(user *ent.User, projectID int, requiredScope ScopeSlug) bool {
21	if user.IsOwner {
22		return true
23	}
24
25	// Check if user has project membership with required scope
26	membership, found := lo.Find(user.Edges.ProjectUsers, func(projectUser *ent.UserProject) bool {
27		return projectUser.ProjectID == projectID
28	})
29
30	if !found {
31		return false
32	}
33
34	if membership.IsOwner {
35		return true
36	}
37
38	if hasScope(membership.Scopes, string(requiredScope)) {
39		return true
40	}
41
42	for _, role := range user.Edges.Roles {
43		if role.ProjectID != nil && *role.ProjectID == projectID {
44			if hasScope(role.Scopes, string(requiredScope)) {
45				return true
46			}
47		}
48	}
49
50	return false
51}
52
53// UserProjectScopeReadRule allows users to query projects they are members of.
54// It checks:
55// 1. If user has global scope permission -> Allow all
56// 2. If user is project member with required scope -> Filter by project membership.
57func UserProjectScopeReadRule(requiredScope ScopeSlug) privacy.QueryRule {
58	return privacy.FilterFunc(projectMemberQueryFilter(requiredScope))
59}
60
61func projectMemberQueryFilter(requiredScope ScopeSlug) func(ctx context.Context, q privacy.Filter) error {
62	return func(ctx context.Context, q privacy.Filter) error {
63		// Check if project ID is in context
64		projectID, hasProjectID := contexts.GetProjectID(ctx)
65		if !hasProjectID {
66			return privacy.Skipf("Project ID not found in context")
67		}
68
69		currentUser, err := getUserFromContext(ctx)
70		if err != nil {
71			return err
72		}
73
74		switch q := q.(type) {
75		case ProjectOwnedFilter:
76			// Check if user has global scope permission or project scope permission.
77			if !userHasSystemScope(currentUser, requiredScope) && !userHasProjectScope(currentUser, projectID, requiredScope) {
78				return privacy.Skipf("User %d can not query project %d with scope %s", currentUser.ID, projectID, requiredScope)
79			}
80
81			q.WhereProjectID(entql.IntEQ(projectID))
82
83			return privacy.Allowf("User %d can query project %d with scope %s", currentUser.ID, projectID, requiredScope)
84		case *ent.ProjectFilter:
85			if !userHasSystemScope(currentUser, requiredScope) && !userHasProjectScope(currentUser, projectID, requiredScope) {
86				return privacy.Skipf("User %d can not query project %d with scope %s", currentUser.ID, projectID, requiredScope)
87			}
88
89			q.WhereID(entql.IntEQ(projectID))
90
91			return privacy.Allowf("User %d can query project %d with scope %s", currentUser.ID, projectID, requiredScope)
92		default:
93			return privacy.Skipf("User %d can only query project %d with scope %s", currentUser.ID, projectID, requiredScope)
94		}
95	}
96}
97
98// UserProjectScopeWriteRule ensures users can only modify resources in projects they are members of.
99func UserProjectScopeWriteRule(requiredScope ScopeSlug) privacy.MutationRule {
100	return projectMemberMutationRule{requiredScope: requiredScope}
101}
102
103type ProjectOwnedMutation interface {
104	ent.Mutation
105	ProjectID() (r int, exists bool)
106	WhereP(ps ...func(*sql.Selector))
107}
108
109type projectMemberMutationRule struct {
110	requiredScope ScopeSlug
111}
112
113func (r projectMemberMutationRule) EvalMutation(ctx context.Context, m ent.Mutation) error {
114	user, err := getUserFromContext(ctx)
115	if err != nil {
116		return privacy.Skipf("User not found in context")
117	}
118
119	// For mutations, check project membership
120	switch mutation := m.(type) {
121	case ProjectOwnedMutation:
122		projectID, hasProjectID := contexts.GetProjectID(ctx)
123		if !hasProjectID {
124			return privacy.Skipf("Project ID not found in context")
125		}
126
127		if !userHasSystemScope(user, r.requiredScope) && !userHasProjectScope(user, projectID, r.requiredScope) {
128			return privacy.Skipf("User %d can not modify resources in project %d with scope %s", user.ID, projectID, r.requiredScope)
129		}
130
131		switch mutation.Op() {
132		case ent.OpCreate:
133			mProjectID, ok := mutation.ProjectID()
134			if !ok {
135				return privacy.Skipf("Project ID not found")
136			}
137
138			if mProjectID != projectID {
139				return privacy.Skipf("User %d can not create resources in project %d with scope %s", user.ID, mProjectID, r.requiredScope)
140			}
141
142			return privacy.Allowf("User %d can create resources in project %d", user.ID, mProjectID)
143		case ent.OpUpdateOne, ent.OpDeleteOne:
144			mutation.WhereP(func(s *sql.Selector) {
145				s.Where(sql.EQ("project_id", projectID))
146			})
147
148			return privacy.Allowf("User %d can modify resources in project %d", user.ID, projectID)
149		case ent.OpUpdate, ent.OpDelete:
150			mutation.WhereP(func(s *sql.Selector) {
151				s.Where(sql.EQ("project_id", projectID))
152			})
153
154			return privacy.Allowf("User %d can modify resources in project %d", user.ID, projectID)
155		default:
156			return privacy.Denyf("Unsupported operation %s", mutation.Op())
157		}
158	case *ent.ProjectMutation:
159		// Check if user has global scope permission
160		if userHasSystemScope(user, r.requiredScope) {
161			return privacy.Allowf("User %d can create project", user.ID)
162		}
163
164		if mutation.Op().Is(ent.OpCreate) {
165			return privacy.Skipf("User %d can not create project", user.ID)
166		}
167
168		mProjectID, mProjectIDExists := mutation.ID()
169		if !mProjectIDExists {
170			return privacy.Skipf("Project ID not found")
171		}
172
173		if userHasProjectScope(user, mProjectID, r.requiredScope) {
174			return privacy.Allowf("User %d can modify project %d", user.ID, mProjectID)
175		}
176
177		return privacy.Skipf("User %d can not modify project %d", user.ID, mProjectID)
178	default:
179		return privacy.Skipf("Not a project-related mutation")
180	}
181}
182