|
| 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