Skip to content

Commit bf64338

Browse files
authored
UUIDs and Org Scoping (#2329)
* wip * add org scoping, test to demo * adjust db helper and initialization
1 parent 65b1016 commit bf64338

37 files changed

+2149
-1197
lines changed

taco/cmd/statesman/main.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,21 @@ func main() {
111111

112112
// create repository
113113
// repository coordinates blob storage with query index internally
114-
repo := repositories.NewUnitRepository(blobStore, queryStore)
115-
log.Println("Repository initialized (coordinates blob + query)")
114+
// Get the underlying *gorm.DB from the query store
115+
db := repositories.GetDBFromQueryStore(queryStore)
116+
if db == nil {
117+
log.Fatalf("Query store does not provide GetDB method")
118+
}
119+
120+
// Ensure default organization exists
121+
defaultOrgUUID, err := repositories.EnsureDefaultOrganization(context.Background(), db)
122+
if err != nil {
123+
log.Fatalf("Failed to ensure default organization: %v", err)
124+
}
125+
log.Printf("Default organization ensured: %s", defaultOrgUUID)
126+
127+
repo := repositories.NewUnitRepository(db, blobStore)
128+
log.Println("Repository initialized (database-first with blob storage backend)")
116129

117130
// Create RBAC Manager
118131
rbacManager, err := rbac.NewRBACManagerFromQueryStore(queryStore)
@@ -128,8 +141,12 @@ func main() {
128141
if !*authDisable {
129142
log.Println("Authorization is ENABLED. Wrapping repository with RBAC.")
130143

144+
// Create bootstrap context with default org for RBAC check
145+
// During startup, we need org context to check RBAC status
146+
bootstrapCtx := domain.ContextWithOrg(context.Background(), defaultOrgUUID)
147+
131148
// Verify RBAC manager was created successfully (fail closed for security)
132-
canInit, err := rbacManager.IsEnabled(context.Background())
149+
canInit, err := rbacManager.IsEnabled(bootstrapCtx)
133150
if err != nil {
134151
log.Fatalf("Failed to verify RBAC manager: %v", err)
135152
}
@@ -144,8 +161,8 @@ func main() {
144161
}
145162

146163
// Initialize analytics with system ID management (always create system ID)
147-
// Analytics uses the full repository for storage operations
148-
analytics.InitGlobalWithSystemID("production", fullRepo)
164+
// Analytics uses the blob store for storage operations
165+
analytics.InitGlobalWithSystemID("production", blobStore)
149166
// Initialize system ID synchronously during startup
150167
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
151168
defer cancel()
@@ -184,7 +201,8 @@ func main() {
184201

185202
// Register routes with interface-based dependencies
186203
api.RegisterRoutes(e, api.Dependencies{
187-
Repository: fullRepo, // Coordinated unit operations
204+
Repository: fullRepo, // RBAC-wrapped repository (used by all routes)
205+
BlobStore: blobStore, // Direct blob access (for legacy components)
188206
QueryStore: queryStore, // Direct query access
189207
RBACManager: rbacManager, // RBAC management
190208
Signer: signer, // JWT signing

taco/cmd/taco/commands/login.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,24 @@ func exchangeCodeForTokens(tokenURL, clientID, redirectURI, code, verifier strin
231231
resp, err := http.DefaultClient.Do(req)
232232
if err != nil { return nil, err }
233233
defer resp.Body.Close()
234-
if resp.StatusCode != 200 { return nil, fmt.Errorf("token http %d", resp.StatusCode) }
234+
235+
// Read response body for error details
236+
body, _ := io.ReadAll(resp.Body)
237+
238+
if resp.StatusCode != 200 {
239+
// Try to parse Auth0/OIDC error response
240+
var errResp struct {
241+
Error string `json:"error"`
242+
ErrorDescription string `json:"error_description"`
243+
}
244+
if json.Unmarshal(body, &errResp) == nil && errResp.Error != "" {
245+
return nil, fmt.Errorf("token exchange failed (HTTP %d): %s - %s", resp.StatusCode, errResp.Error, errResp.ErrorDescription)
246+
}
247+
return nil, fmt.Errorf("token http %d: %s", resp.StatusCode, string(body))
248+
}
249+
235250
var out struct{ IDToken string `json:"id_token"` }
236-
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return nil, err }
251+
if err := json.Unmarshal(body, &out); err != nil { return nil, err }
237252
return &out, nil
238253
}
239254

taco/internal/api/internal.go

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,22 @@ func RegisterInternalRoutes(e *echo.Echo, deps Dependencies) {
3232
userRepo = repositories.NewUserRepositoryFromQueryStore(deps.QueryStore)
3333
}
3434

35-
// Create internal group with webhook auth (with orgRepo for existence check)
35+
// Create internal group with webhook auth
3636
internal := e.Group("/internal/api")
37-
internal.Use(middleware.WebhookAuth(orgRepo))
37+
internal.Use(middleware.WebhookAuth())
38+
39+
// Add org resolution middleware - resolves org name to UUID and adds to domain context
40+
if deps.QueryStore != nil {
41+
if db := repositories.GetDBFromQueryStore(deps.QueryStore); db != nil {
42+
// Create identifier resolver (infrastructure layer)
43+
identifierResolver := repositories.NewIdentifierResolver(db)
44+
// Pass interface to middleware (clean architecture!)
45+
internal.Use(middleware.ResolveOrgContextMiddleware(identifierResolver))
46+
log.Println("Org context resolution middleware enabled for internal routes")
47+
} else {
48+
log.Println("WARNING: QueryStore does not implement GetDB() *gorm.DB - org resolution disabled")
49+
}
50+
}
3851

3952
// Organization and User management endpoints
4053
if orgRepo != nil && userRepo != nil {
@@ -70,50 +83,44 @@ func RegisterInternalRoutes(e *echo.Echo, deps Dependencies) {
7083
log.Println("RBAC management endpoints registered at /internal/api/rbac")
7184
}
7285

73-
orgService := domain.NewOrgService()
74-
orgScopedRepo := repositories.NewOrgScopedRepository(deps.Repository, orgService)
86+
// For internal routes, use RBAC-wrapped repository
87+
// Architecture:
88+
// - Webhook secret authenticates the SYSTEM (backend orchestrator)
89+
// - X-User-ID header identifies the END USER making the request
90+
// - RBAC enforces what that USER can do (via repository layer)
91+
// - Org scoping handled by middleware (ResolveOrgContextMiddleware) + database foreign keys
92+
93+
// Create identifier resolver for unit name→UUID resolution
94+
var identifierResolver domain.IdentifierResolver
95+
if deps.QueryStore != nil {
96+
if db := repositories.GetDBFromQueryStore(deps.QueryStore); db != nil {
97+
identifierResolver = repositories.NewIdentifierResolver(db)
98+
}
99+
}
75100

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
101+
// Create handler with org-scoped + RBAC-wrapped repository
80102
unitHandler := unithandlers.NewHandler(
81-
domain.UnitManagement(orgScopedRepo),
103+
domain.UnitManagement(deps.Repository), // Use RBAC-wrapped repository directly
104+
deps.BlobStore,
82105
deps.RBACManager,
83106
deps.Signer,
84107
deps.QueryStore,
108+
identifierResolver,
85109
)
86110

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-
}
111+
// Internal routes with RBAC enforcement
112+
// Note: Users must have permissions assigned via /internal/api/rbac endpoints
113+
internal.POST("/units", unitHandler.CreateUnit)
114+
internal.GET("/units", unitHandler.ListUnits)
115+
internal.GET("/units/:id", unitHandler.GetUnit)
116+
internal.DELETE("/units/:id", unitHandler.DeleteUnit)
117+
internal.GET("/units/:id/download", unitHandler.DownloadUnit)
118+
internal.POST("/units/:id/upload", unitHandler.UploadUnit)
119+
internal.POST("/units/:id/lock", unitHandler.LockUnit)
120+
internal.DELETE("/units/:id/unlock", unitHandler.UnlockUnit)
121+
internal.GET("/units/:id/status", unitHandler.GetUnitStatus)
122+
internal.GET("/units/:id/versions", unitHandler.ListVersions)
123+
internal.POST("/units/:id/restore", unitHandler.RestoreVersion)
117124

118125
// Health check for internal routes
119126
internal.GET("/health", func(c echo.Context) error {
@@ -153,7 +160,6 @@ func RegisterInternalRoutes(e *echo.Echo, deps Dependencies) {
153160

154161
log.Printf("Internal routes registered at /internal/api/* with webhook authentication")
155162
}
156-
157163
// wrapWithWebhookRBAC wraps a handler with RBAC permission checking
158164
func wrapWithWebhookRBAC(manager *rbac.RBACManager, action rbac.Action, resource string) func(echo.HandlerFunc) echo.HandlerFunc {
159165
return func(next echo.HandlerFunc) echo.HandlerFunc {
@@ -208,3 +214,4 @@ func wrapWithWebhookRBAC(manager *rbac.RBACManager, action rbac.Action, resource
208214
}
209215
}
210216
}
217+

taco/internal/api/org_handler.go

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,17 @@ func NewOrgHandler(orgRepo domain.OrganizationRepository, userRepo domain.UserRe
2929

3030
// CreateOrgRequest is the request body for creating an organization
3131
type CreateOrgRequest struct {
32-
OrgID string `json:"org_id" validate:"required"`
33-
Name string `json:"name" validate:"required"`
32+
Name string `json:"name" validate:"required"` // Unique identifier (e.g., "acme")
33+
DisplayName string `json:"display_name" validate:"required"` // Friendly name (e.g., "Acme Corp")
3434
}
3535

3636
// CreateOrgResponse is the response for creating an organization
3737
type CreateOrgResponse struct {
38-
OrgID string `json:"org_id"`
39-
Name string `json:"name"`
40-
CreatedBy string `json:"created_by"`
41-
CreatedAt string `json:"created_at"`
38+
ID string `json:"id"` // UUID
39+
Name string `json:"name"` // Unique identifier
40+
DisplayName string `json:"display_name"` // Friendly name
41+
CreatedBy string `json:"created_by"`
42+
CreatedAt string `json:"created_at"`
4243
}
4344

4445
// CreateOrganization handles POST /internal/orgs
@@ -82,15 +83,15 @@ func (h *OrgHandler) CreateOrganization(c echo.Context) error {
8283
})
8384
}
8485

85-
if req.OrgID == "" || req.Name == "" {
86+
if req.Name == "" || req.DisplayName == "" {
8687
return c.JSON(http.StatusBadRequest, map[string]string{
87-
"error": "org_id and name are required",
88+
"error": "name and display_name are required",
8889
})
8990
}
9091

9192
slog.Info("Creating organization",
92-
"orgID", req.OrgID,
9393
"name", req.Name,
94+
"displayName", req.DisplayName,
9495
"createdBy", userIDStr,
9596
)
9697

@@ -101,7 +102,7 @@ func (h *OrgHandler) CreateOrganization(c echo.Context) error {
101102

102103
// Create organization in transaction
103104
err := h.orgRepo.WithTransaction(ctx, func(ctx context.Context, txRepo domain.OrganizationRepository) error {
104-
createdOrg, err := txRepo.Create(ctx, req.OrgID, req.Name, userIDStr)
105+
createdOrg, err := txRepo.Create(ctx, req.Name, req.DisplayName, userIDStr)
105106
if err != nil {
106107
return err
107108
}
@@ -123,7 +124,7 @@ func (h *OrgHandler) CreateOrganization(c echo.Context) error {
123124
}
124125

125126
slog.Error("Failed to create organization",
126-
"orgID", req.OrgID,
127+
"name", req.Name,
127128
"error", err,
128129
)
129130
return c.JSON(http.StatusInternalServerError, map[string]string{
@@ -135,33 +136,37 @@ func (h *OrgHandler) CreateOrganization(c echo.Context) error {
135136
// Initialize RBAC after org creation (outside transaction for SQLite compatibility)
136137
if h.rbacManager != nil {
137138
slog.Info("Initializing RBAC for new organization",
138-
"orgID", req.OrgID,
139+
"orgName", req.Name,
140+
"orgID", org.ID,
139141
"adminUser", userIDStr,
140142
)
141143

142-
if err := h.rbacManager.InitializeRBAC(ctx, userIDStr, emailStr); err != nil {
144+
if err := h.rbacManager.InitializeRBAC(ctx, org.ID, userIDStr, emailStr); err != nil {
143145
// Org was created but RBAC failed - log warning but don't fail the request
144146
// User can retry RBAC initialization or assign roles manually
145147
slog.Warn("Organization created but RBAC initialization failed",
146-
"orgID", req.OrgID,
148+
"orgName", req.Name,
149+
"orgID", org.ID,
147150
"error", err,
148151
"recommendation", "RBAC can be initialized later via /rbac/init endpoint",
149152
)
150153
// Continue with success response - org was created
151154
} else {
152155
slog.Info("RBAC initialized successfully",
153-
"orgID", req.OrgID,
156+
"orgName", req.Name,
157+
"orgID", org.ID,
154158
"adminUser", userIDStr,
155159
)
156160
}
157161
}
158162

159163
// Success - org created (and RBAC initialized if available)
160164
return c.JSON(http.StatusCreated, CreateOrgResponse{
161-
OrgID: org.OrgID,
162-
Name: org.Name,
163-
CreatedBy: org.CreatedBy,
164-
CreatedAt: org.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
165+
ID: org.ID,
166+
Name: org.Name,
167+
DisplayName: org.DisplayName,
168+
CreatedBy: org.CreatedBy,
169+
CreatedAt: org.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
165170
})
166171
}
167172

@@ -193,11 +198,12 @@ func (h *OrgHandler) GetOrganization(c echo.Context) error {
193198
}
194199

195200
return c.JSON(http.StatusOK, map[string]interface{}{
196-
"org_id": org.OrgID,
197-
"name": org.Name,
198-
"created_by": org.CreatedBy,
199-
"created_at": org.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
200-
"updated_at": org.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
201+
"id": org.ID,
202+
"name": org.Name,
203+
"display_name": org.DisplayName,
204+
"created_by": org.CreatedBy,
205+
"created_at": org.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
206+
"updated_at": org.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
201207
})
202208
}
203209

@@ -216,11 +222,12 @@ func (h *OrgHandler) ListOrganizations(c echo.Context) error {
216222
response := make([]map[string]interface{}, len(orgs))
217223
for i, org := range orgs {
218224
response[i] = map[string]interface{}{
219-
"org_id": org.OrgID,
220-
"name": org.Name,
221-
"created_by": org.CreatedBy,
222-
"created_at": org.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
223-
"updated_at": org.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
225+
"id": org.ID,
226+
"name": org.Name,
227+
"display_name": org.DisplayName,
228+
"created_by": org.CreatedBy,
229+
"created_at": org.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
230+
"updated_at": org.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
224231
}
225232
}
226233

0 commit comments

Comments
 (0)