mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-15 08:02:42 +00:00
feat: enforce grant-scoped organization summaries and account-wide workspace creation with target membership checks for limit requests
This commit is contained in:
committed by
Devin AI
parent
3ececd2934
commit
8d4e1de4e4
@@ -16,12 +16,19 @@ const MemberAccessRestricted = "member_access_restricted"
|
||||
var errMemberAccessRestricted = errx.NewWithIdentifier(errx.Forbidden, MemberAccessRestricted,
|
||||
"Your access to this workspace is limited to selected campaigns and mailboxes, and this is outside it.")
|
||||
|
||||
// These services authorize the account or target workspace, not the currently selected membership.
|
||||
var organizationPolicyRoutes = map[string]bool{
|
||||
"POST /v1/organization": true,
|
||||
"POST /v1/organization/:orgId/limit-requests": true,
|
||||
"GET /v1/organization/:orgId/limit-requests": true,
|
||||
"DELETE /v1/limit-requests/:id": true,
|
||||
}
|
||||
|
||||
// scopeAwareRoutes is every route a restricted member may call; each one filters to the member's grants.
|
||||
var scopeAwareRoutes = map[string]bool{
|
||||
// The member's own account, workspace list and socket.
|
||||
"GET /v1/me": true,
|
||||
"GET /v1/organization": true,
|
||||
"POST /v1/organization": true,
|
||||
"POST /v1/organization/switch/:id": true,
|
||||
"GET /v1/organization/current": true,
|
||||
"GET /v1/me/views/:view": true,
|
||||
@@ -60,18 +67,25 @@ var scopeAwareRoutes = map[string]bool{
|
||||
"GET /v1/unibox/:id": true,
|
||||
}
|
||||
|
||||
// ScopeAwareRoutes lists the routes a restricted member may call, for the route-table test.
|
||||
// ScopeAwareRoutes lists grant-aware and target-authorized routes for the route-table test.
|
||||
func ScopeAwareRoutes() []string {
|
||||
out := make([]string, 0, len(scopeAwareRoutes))
|
||||
out := make([]string, 0, len(scopeAwareRoutes)+len(organizationPolicyRoutes))
|
||||
for r := range scopeAwareRoutes {
|
||||
out = append(out, r)
|
||||
}
|
||||
for r := range organizationPolicyRoutes {
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ResourceScopeGate holds a restricted member, and any key or app acting for one, to scope-aware routes and their grants.
|
||||
func (h *Handler) ResourceScopeGate() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if organizationPolicyRoutes[c.Request.Method+" "+c.FullPath()] {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
m := GetAuthMember(c)
|
||||
if !m.IsRestricted() {
|
||||
c.Next()
|
||||
@@ -93,6 +107,11 @@ func (h *Handler) ResourceScopeGate() gin.HandlerFunc {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if scope == nil {
|
||||
errx.JSON(c, errx.InternalError())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set(ResourceScopeKey, scope)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
@@ -19,11 +19,52 @@ type scopeOrgs struct {
|
||||
organization.OrganizationService
|
||||
scope *models.ResourceScope
|
||||
resolved int
|
||||
err *errx.Error
|
||||
}
|
||||
|
||||
func (f *scopeOrgs) ResolveMemberScope(context.Context, uuid.UUID, uuid.UUID) (*models.ResourceScope, *errx.Error) {
|
||||
f.resolved++
|
||||
return f.scope, nil
|
||||
return f.scope, f.err
|
||||
}
|
||||
|
||||
func TestResourceScopeGateFailsClosedOnResolutionFailure(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
err *errx.Error
|
||||
}{
|
||||
{"nil scope", nil},
|
||||
{"resolver error", errx.InternalError()},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
orgs := &scopeOrgs{err: tc.err}
|
||||
rec, seen := serveScoped(t, &Handler{OrganizationService: orgs}, restrictedMember(uuid.New()), nil, http.MethodGet, "/v1/organization/current", "/v1/organization/current")
|
||||
if rec.Code != http.StatusInternalServerError || seen != nil || orgs.resolved != 1 {
|
||||
t.Fatalf("resolution failure proceeded: HTTP %d seen=%v resolved=%d", rec.Code, seen != nil, orgs.resolved)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceScopeGateDefersCreationToAccountPolicy(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
for _, auth := range []string{AuthTypeJWT, AuthTypeAPIKey, AuthTypeOAuth} {
|
||||
t.Run(auth, func(t *testing.T) {
|
||||
orgs := &scopeOrgs{err: errx.InternalError()}
|
||||
h := &Handler{OrganizationService: orgs}
|
||||
r := gin.New()
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Set(AuthTypeKey, auth)
|
||||
c.Set(SessionMemberKey, restrictedMember(uuid.New()))
|
||||
})
|
||||
r.POST("/v1/organization", h.ResourceScopeGate(), func(c *gin.Context) { c.Status(http.StatusAccepted) })
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/v1/organization", nil))
|
||||
if w.Code != http.StatusAccepted || orgs.resolved != 0 {
|
||||
t.Fatalf("creation was tied to selected member resolution: HTTP %d resolved=%d", w.Code, orgs.resolved)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func restrictedMember(orgID uuid.UUID) *models.OrganizationMember {
|
||||
|
||||
Reference in New Issue
Block a user