Skip to content

Commit e58f9d7

Browse files
authored
Webhook auth and internal endpoints (#2319)
* changes * read and write working * add lightstream example * query wip * EOD wip * add db types, modular bucket, auth * remove duplicates * remove noop store * remove unit ls-fast test command * remove units fast from the client * remove WAL setup * remove unnecessary * adjust auth * wip - mssql fix * enable NULL for lock time * some changes to documentation * Test Suite for query engine, adjustments to stores Discovered a flaw in sqlstore's handling of wildcard perms during testing, adjusted Adjustd the s3 store to enable testing with this definition instead of separate mock Added tests for different aspects of the query engine, especially rbac and unit management. * fix docker warnings and build fail * add missing syncs for rbac * adjust docs, silence SQL logs, check for existence before sync * adjust docs - PATH to DB_PATH, add prefix * refactor with repository in mind * add webhook auth, internal routing * some how have double models.go after merging * add constant time check, verify org, other corrections * revert, other adjustments * restore from develop * remove comment
1 parent 519a74c commit e58f9d7

File tree

12 files changed

+1691
-0
lines changed

12 files changed

+1691
-0
lines changed

taco/internal/api/internal.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package api
2+
3+
import (
4+
"log"
5+
"net/http"
6+
"os"
7+
8+
"github.com/diggerhq/digger/opentaco/internal/domain"
9+
"github.com/diggerhq/digger/opentaco/internal/middleware"
10+
"github.com/diggerhq/digger/opentaco/internal/rbac"
11+
"github.com/diggerhq/digger/opentaco/internal/repositories"
12+
unithandlers "github.com/diggerhq/digger/opentaco/internal/unit"
13+
"github.com/labstack/echo/v4"
14+
)
15+
16+
17+
func RegisterInternalRoutes(e *echo.Echo, deps Dependencies) {
18+
webhookSecret := os.Getenv("OPENTACO_WEBHOOK_SECRET")
19+
if webhookSecret == "" {
20+
log.Println("OPENTACO_WEBHOOK_SECRET not configured, skipping internal routes")
21+
return
22+
}
23+
24+
log.Println("Registering internal routes with webhook authentication")
25+
26+
// Create repositories first (needed for webhook middleware)
27+
var orgRepo domain.OrganizationRepository
28+
var userRepo domain.UserRepository
29+
30+
if deps.QueryStore != nil {
31+
orgRepo = repositories.NewOrgRepositoryFromQueryStore(deps.QueryStore)
32+
userRepo = repositories.NewUserRepositoryFromQueryStore(deps.QueryStore)
33+
}
34+
35+
// Create internal group with webhook auth (with orgRepo for existence check)
36+
internal := e.Group("/internal")
37+
internal.Use(middleware.WebhookAuth(orgRepo))
38+
39+
// Organization and User management endpoints
40+
if orgRepo != nil && userRepo != nil {
41+
// Create handler with repository interfaces (domain layer)
42+
orgHandler := NewOrgHandler(orgRepo, userRepo, deps.RBACManager)
43+
44+
// Organization endpoints
45+
internal.POST("/orgs", orgHandler.CreateOrganization)
46+
internal.GET("/orgs/:orgId", orgHandler.GetOrganization)
47+
internal.GET("/orgs", orgHandler.ListOrganizations)
48+
49+
// User endpoints
50+
internal.POST("/users", orgHandler.CreateUser)
51+
internal.GET("/users/:subject", orgHandler.GetUser)
52+
internal.GET("/users", orgHandler.ListUsers)
53+
54+
log.Println("Organization management endpoints registered at /internal/orgs")
55+
log.Println("User management endpoints registered at /internal/users")
56+
} else {
57+
log.Println("Warning: Could not create org/user repositories, endpoints disabled")
58+
}
59+
60+
// Reuse existing RBAC handler with webhook auth (no duplication)
61+
if deps.RBACManager != nil {
62+
rbacHandler := rbac.NewHandler(deps.RBACManager, deps.Signer, deps.QueryStore)
63+
rbacGroup := internal.Group("/rbac")
64+
rbacGroup.POST("/roles", rbacHandler.CreateRole)
65+
rbacGroup.GET("/roles", rbacHandler.ListRoles)
66+
rbacGroup.POST("/permissions", rbacHandler.CreatePermission)
67+
rbacGroup.GET("/permissions", rbacHandler.ListPermissions)
68+
rbacGroup.POST("/assign", rbacHandler.AssignRole)
69+
rbacGroup.POST("/revoke", rbacHandler.RevokeRole)
70+
log.Println("RBAC management endpoints registered at /internal/rbac")
71+
}
72+
73+
orgService := domain.NewOrgService()
74+
orgScopedRepo := repositories.NewOrgScopedRepository(deps.Repository, orgService)
75+
76+
// Create handler with org-scoped repository
77+
// The repository will automatically:
78+
// - Filter List() to org namespace
79+
// - Validate all operations belong to user's org
80+
unitHandler := unithandlers.NewHandler(
81+
domain.UnitManagement(orgScopedRepo),
82+
deps.RBACManager,
83+
deps.Signer,
84+
deps.QueryStore,
85+
)
86+
87+
88+
89+
90+
if deps.RBACManager != nil {
91+
// With RBAC - apply RBAC permission checks
92+
// Org scoping is automatic via orgScopedRepository
93+
internal.POST("/units", wrapWithWebhookRBAC(deps.RBACManager, rbac.ActionUnitWrite, "*")(unitHandler.CreateUnit))
94+
internal.GET("/units", unitHandler.ListUnits) // Automatically filters by org
95+
internal.GET("/units/:id", wrapWithWebhookRBAC(deps.RBACManager, rbac.ActionUnitRead, "{id}")(unitHandler.GetUnit))
96+
internal.DELETE("/units/:id", wrapWithWebhookRBAC(deps.RBACManager, rbac.ActionUnitDelete, "{id}")(unitHandler.DeleteUnit))
97+
internal.GET("/units/:id/download", wrapWithWebhookRBAC(deps.RBACManager, rbac.ActionUnitRead, "{id}")(unitHandler.DownloadUnit))
98+
internal.POST("/units/:id/upload", wrapWithWebhookRBAC(deps.RBACManager, rbac.ActionUnitWrite, "{id}")(unitHandler.UploadUnit))
99+
internal.POST("/units/:id/lock", wrapWithWebhookRBAC(deps.RBACManager, rbac.ActionUnitLock, "{id}")(unitHandler.LockUnit))
100+
internal.DELETE("/units/:id/unlock", wrapWithWebhookRBAC(deps.RBACManager, rbac.ActionUnitLock, "{id}")(unitHandler.UnlockUnit))
101+
internal.GET("/units/:id/status", wrapWithWebhookRBAC(deps.RBACManager, rbac.ActionUnitRead, "{id}")(unitHandler.GetUnitStatus))
102+
internal.GET("/units/:id/versions", wrapWithWebhookRBAC(deps.RBACManager, rbac.ActionUnitRead, "{id}")(unitHandler.ListVersions))
103+
internal.POST("/units/:id/restore", wrapWithWebhookRBAC(deps.RBACManager, rbac.ActionUnitWrite, "{id}")(unitHandler.RestoreVersion))
104+
} else {
105+
internal.POST("/units", unitHandler.CreateUnit)
106+
internal.GET("/units", unitHandler.ListUnits)
107+
internal.GET("/units/:id", unitHandler.GetUnit)
108+
internal.DELETE("/units/:id", unitHandler.DeleteUnit)
109+
internal.GET("/units/:id/download", unitHandler.DownloadUnit)
110+
internal.POST("/units/:id/upload", unitHandler.UploadUnit)
111+
internal.POST("/units/:id/lock", unitHandler.LockUnit)
112+
internal.DELETE("/units/:id/unlock", unitHandler.UnlockUnit)
113+
internal.GET("/units/:id/status", unitHandler.GetUnitStatus)
114+
internal.GET("/units/:id/versions", unitHandler.ListVersions)
115+
internal.POST("/units/:id/restore", unitHandler.RestoreVersion)
116+
}
117+
118+
// Health check for internal routes
119+
internal.GET("/health", func(c echo.Context) error {
120+
return c.JSON(http.StatusOK, map[string]interface{}{
121+
"status": "ok",
122+
"type": "internal",
123+
"auth_type": "webhook",
124+
})
125+
})
126+
127+
// Info endpoint that shows current user context
128+
internal.GET("/me", func(c echo.Context) error {
129+
userID := c.Get("user_id")
130+
email := c.Get("email")
131+
orgID := c.Get("organization_id")
132+
133+
// Get principal from context
134+
principal, hasPrincipal := rbac.PrincipalFromContext(c.Request().Context())
135+
136+
info := map[string]interface{}{
137+
"user_id": userID,
138+
"email": email,
139+
"org_id": orgID,
140+
}
141+
142+
if hasPrincipal {
143+
info["principal"] = map[string]interface{}{
144+
"subject": principal.Subject,
145+
"email": principal.Email,
146+
"roles": principal.Roles,
147+
"groups": principal.Groups,
148+
}
149+
}
150+
151+
return c.JSON(http.StatusOK, info)
152+
})
153+
154+
log.Printf("Internal routes registered at /internal/* with webhook authentication")
155+
}
156+
157+
// wrapWithWebhookRBAC wraps a handler with RBAC permission checking
158+
func wrapWithWebhookRBAC(manager *rbac.RBACManager, action rbac.Action, resource string) func(echo.HandlerFunc) echo.HandlerFunc {
159+
return func(next echo.HandlerFunc) echo.HandlerFunc {
160+
return func(c echo.Context) error {
161+
// Get principal from context (injected by webhook middleware)
162+
principal, ok := rbac.PrincipalFromContext(c.Request().Context())
163+
if !ok {
164+
return c.JSON(http.StatusUnauthorized, map[string]string{
165+
"error": "no principal in context",
166+
})
167+
}
168+
169+
// Check if RBAC is enabled
170+
enabled, err := manager.IsEnabled(c.Request().Context())
171+
if err != nil {
172+
return c.JSON(http.StatusInternalServerError, map[string]string{
173+
"error": "failed to check RBAC status",
174+
})
175+
}
176+
177+
// If RBAC is not enabled, allow access
178+
if !enabled {
179+
return next(c)
180+
}
181+
182+
// Resolve resource pattern (e.g., "{id}" -> actual ID from path param)
183+
resolvedResource := resource
184+
if resource == "{id}" {
185+
resolvedResource = c.Param("id")
186+
} else if resource == "*" {
187+
// For wildcard resources, use the path or a default
188+
resolvedResource = c.Request().URL.Path
189+
}
190+
191+
// Check permission
192+
allowed, err := manager.Can(c.Request().Context(), principal, action, resolvedResource)
193+
if err != nil {
194+
return c.JSON(http.StatusInternalServerError, map[string]string{
195+
"error": "failed to check permission",
196+
})
197+
}
198+
199+
if !allowed {
200+
return c.JSON(http.StatusForbidden, map[string]string{
201+
"error": "permission denied",
202+
"action": string(action),
203+
"resource": resolvedResource,
204+
})
205+
}
206+
207+
return next(c)
208+
}
209+
}
210+
}

0 commit comments

Comments
 (0)