Flowfile 0.5.3__py3-none-any.whl → 0.5.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. flowfile/__init__.py +16 -0
  2. flowfile/web/static/assets/{AdminView-49392a9a.js → AdminView-f9847d67.js} +1 -1
  3. flowfile/web/static/assets/{CloudConnectionView-36bcd6df.css → CloudConnectionView-cf85f943.css} +17 -17
  4. flowfile/web/static/assets/{CloudConnectionView-f13f202b.js → CloudConnectionView-faace55b.js} +4 -4
  5. flowfile/web/static/assets/{CloudStorageReader-0023d4a5.js → CloudStorageReader-d86ecaa7.js} +8 -8
  6. flowfile/web/static/assets/{CloudStorageWriter-8e781e11.js → CloudStorageWriter-0f4d9a44.js} +8 -8
  7. flowfile/web/static/assets/ColumnActionInput-c44b7aee.css +159 -0
  8. flowfile/web/static/assets/ColumnActionInput-f4189ae0.js +330 -0
  9. flowfile/web/static/assets/{ColumnSelector-8ad68ea9.js → ColumnSelector-e66b33da.js} +1 -1
  10. flowfile/web/static/assets/ContextMenu-49463352.js +9 -0
  11. flowfile/web/static/assets/ContextMenu-dd5f3f25.js +9 -0
  12. flowfile/web/static/assets/ContextMenu-f709b884.js +9 -0
  13. flowfile/web/static/assets/ContextMenu.vue_vue_type_script_setup_true_lang-a1bd6314.js +59 -0
  14. flowfile/web/static/assets/{CrossJoin-03df6938.js → CrossJoin-24694b8f.js} +9 -9
  15. flowfile/web/static/assets/{CustomNode-8479239b.js → CustomNode-569d45ff.js} +27 -20
  16. flowfile/web/static/assets/CustomNode-edb9b939.css +42 -0
  17. flowfile/web/static/assets/{DatabaseConnectionSettings-e91df89a.css → DatabaseConnectionSettings-c20a1e16.css} +22 -20
  18. flowfile/web/static/assets/{DatabaseConnectionSettings-869e3efd.js → DatabaseConnectionSettings-cfc08938.js} +4 -4
  19. flowfile/web/static/assets/{DatabaseReader-36898a00.css → DatabaseReader-5bf8c75b.css} +39 -44
  20. flowfile/web/static/assets/{DatabaseReader-c58b9552.js → DatabaseReader-701feabb.js} +13 -13
  21. flowfile/web/static/assets/{DatabaseView-d26a9140.js → DatabaseView-0482e5b5.js} +2 -2
  22. flowfile/web/static/assets/{DatabaseWriter-4d05ddc7.js → DatabaseWriter-16721989.js} +12 -12
  23. flowfile/web/static/assets/{DatabaseWriter-217a99f1.css → DatabaseWriter-bdcf2c8b.css} +27 -25
  24. flowfile/web/static/assets/{DesignerView-a6d0ee84.css → DesignerView-49abb835.css} +391 -339
  25. flowfile/web/static/assets/{DesignerView-e6f5c0e8.js → DesignerView-f64749fb.js} +220 -118
  26. flowfile/web/static/assets/{DocumentationView-2e78ef1b.js → DocumentationView-61bd2990.js} +3 -3
  27. flowfile/web/static/assets/{DocumentationView-fd46c656.css → DocumentationView-9ea6e871.css} +9 -9
  28. flowfile/web/static/assets/{ExploreData-7b54caca.js → ExploreData-e2735b13.js} +7 -7
  29. flowfile/web/static/assets/{ExternalSource-3fa399b2.js → ExternalSource-2535c3b2.js} +9 -9
  30. flowfile/web/static/assets/{ExternalSource-47ab05a3.css → ExternalSource-7ac7373f.css} +17 -17
  31. flowfile/web/static/assets/{Filter-8cbbdbf3.js → Filter-2cdbc93c.js} +9 -9
  32. flowfile/web/static/assets/{Formula-aac42b1e.js → Formula-fcda3c2c.js} +9 -9
  33. flowfile/web/static/assets/{FuzzyMatch-cd9bbfca.js → FuzzyMatch-f8d3b7d3.js} +10 -10
  34. flowfile/web/static/assets/{GraphSolver-c24dec17.css → GraphSolver-4b4d7db9.css} +4 -4
  35. flowfile/web/static/assets/{GraphSolver-c7e6780e.js → GraphSolver-72eaa695.js} +11 -11
  36. flowfile/web/static/assets/GroupBy-5792782d.css +9 -0
  37. flowfile/web/static/assets/{GroupBy-93c5d22b.js → GroupBy-8aa0598b.js} +9 -9
  38. flowfile/web/static/assets/{Join-a19b2de2.js → Join-e40f0ffa.js} +10 -10
  39. flowfile/web/static/assets/{LoginView-0df4ed0a.js → LoginView-5111c9ae.js} +1 -1
  40. flowfile/web/static/assets/{ManualInput-8d3374b2.js → ManualInput-9b6f3224.js} +6 -6
  41. flowfile/web/static/assets/{MultiSelect-ad1b6243.js → MultiSelect-ef28e19e.js} +2 -2
  42. flowfile/web/static/assets/{MultiSelect.vue_vue_type_script_setup_true_lang-e278950d.js → MultiSelect.vue_vue_type_script_setup_true_lang-83b3bbfd.js} +1 -1
  43. flowfile/web/static/assets/{NodeDesigner-5f53be3f.css → NodeDesigner-94cd4dd3.css} +190 -190
  44. flowfile/web/static/assets/{NodeDesigner-40b647c9.js → NodeDesigner-d2b7ee2b.js} +171 -69
  45. flowfile/web/static/assets/{NumericInput-7100234c.js → NumericInput-1d789794.js} +2 -2
  46. flowfile/web/static/assets/{NumericInput.vue_vue_type_script_setup_true_lang-5130219f.js → NumericInput.vue_vue_type_script_setup_true_lang-7775f83e.js} +1 -1
  47. flowfile/web/static/assets/{Output-35e97000.css → Output-692dd25d.css} +10 -10
  48. flowfile/web/static/assets/{Output-f5efd2aa.js → Output-cefef801.js} +13 -12
  49. flowfile/web/static/assets/{Pivot-d981d23c.js → Pivot-bab1b75b.js} +10 -10
  50. flowfile/web/static/assets/{PivotValidation-63de1f73.js → PivotValidation-e7941f91.js} +1 -1
  51. flowfile/web/static/assets/{PivotValidation-39386e95.js → PivotValidation-fba09336.js} +1 -1
  52. flowfile/web/static/assets/{PolarsCode-f9d69217.js → PolarsCode-740e40fa.js} +7 -7
  53. flowfile/web/static/assets/{PopOver-b22f049e.js → PopOver-862d7e28.js} +1 -1
  54. flowfile/web/static/assets/{Read-aec2e377.js → Read-225cc63f.js} +15 -14
  55. flowfile/web/static/assets/{Read-36e7bd51.css → Read-90f366bc.css} +13 -13
  56. flowfile/web/static/assets/{RecordCount-78ed6845.js → RecordCount-ffc71eca.js} +6 -6
  57. flowfile/web/static/assets/{RecordId-2156e890.js → RecordId-a70bb8df.js} +9 -9
  58. flowfile/web/static/assets/{SQLQueryComponent-48c72f5b.js → SQLQueryComponent-15a421f5.js} +3 -3
  59. flowfile/web/static/assets/SQLQueryComponent-edb90b98.css +29 -0
  60. flowfile/web/static/assets/{Sample-1352ca74.js → Sample-6c26afc7.js} +6 -6
  61. flowfile/web/static/assets/{SecretSelector-22b5ff89.js → SecretSelector-ceed9496.js} +2 -2
  62. flowfile/web/static/assets/{SecretsView-17df66ee.js → SecretsView-214d255a.js} +2 -2
  63. flowfile/web/static/assets/{Select-0aee4c54.js → Select-8fc29999.js} +9 -9
  64. flowfile/web/static/assets/{SettingsSection-f2002a6d.js → SettingsSection-3f70e4c3.js} +1 -1
  65. flowfile/web/static/assets/{SettingsSection-0784e157.js → SettingsSection-83090218.js} +1 -1
  66. flowfile/web/static/assets/{SettingsSection-cd341bb6.js → SettingsSection-9f0d1725.js} +1 -1
  67. flowfile/web/static/assets/SetupView-3fa0aa03.js +160 -0
  68. flowfile/web/static/assets/SetupView-e2da3442.css +230 -0
  69. flowfile/web/static/assets/{SingleSelect-460cc0ea.js → SingleSelect-a4a568cb.js} +2 -2
  70. flowfile/web/static/assets/{SingleSelect.vue_vue_type_script_setup_true_lang-30741bb2.js → SingleSelect.vue_vue_type_script_setup_true_lang-c8ebdd33.js} +1 -1
  71. flowfile/web/static/assets/{SliderInput-5d926864.js → SliderInput-be533e71.js} +1 -1
  72. flowfile/web/static/assets/{Sort-3cdc971b.js → Sort-154dad81.js} +9 -9
  73. flowfile/web/static/assets/Sort-4abb7fae.css +9 -0
  74. flowfile/web/static/assets/{TextInput-a2d0bfbd.js → TextInput-454e2bda.js} +2 -2
  75. flowfile/web/static/assets/{TextInput.vue_vue_type_script_setup_true_lang-abad1ca2.js → TextInput.vue_vue_type_script_setup_true_lang-e86510d0.js} +1 -1
  76. flowfile/web/static/assets/{TextToRows-918945f7.js → TextToRows-ea73433d.js} +9 -9
  77. flowfile/web/static/assets/{ToggleSwitch-f0ef5196.js → ToggleSwitch-9d7b30f1.js} +2 -2
  78. flowfile/web/static/assets/{ToggleSwitch.vue_vue_type_script_setup_true_lang-5605c793.js → ToggleSwitch.vue_vue_type_script_setup_true_lang-00f2580e.js} +1 -1
  79. flowfile/web/static/assets/{UnavailableFields-54d2f518.css → UnavailableFields-394a1f78.css} +13 -13
  80. flowfile/web/static/assets/{UnavailableFields-bdad6144.js → UnavailableFields-b72a2c72.js} +4 -4
  81. flowfile/web/static/assets/{Union-e8ab8c86.js → Union-1e44f263.js} +6 -6
  82. flowfile/web/static/assets/Unique-2b705521.css +3 -0
  83. flowfile/web/static/assets/{Unique-8cd4f976.js → Unique-a3bc6d0a.js} +12 -12
  84. flowfile/web/static/assets/{Unpivot-710a2948.css → Unpivot-b6ad6427.css} +6 -6
  85. flowfile/web/static/assets/{Unpivot-8da14095.js → Unpivot-e27935fc.js} +11 -11
  86. flowfile/web/static/assets/{UnpivotValidation-6f7d89ff.js → UnpivotValidation-72497680.js} +1 -1
  87. flowfile/web/static/assets/{VueGraphicWalker-3fb312e1.js → VueGraphicWalker-d9ab70a3.js} +1 -1
  88. flowfile/web/static/assets/{api-24483f0d.js → api-a2102880.js} +1 -1
  89. flowfile/web/static/assets/{api-8b81fa73.js → api-f75042b0.js} +1 -1
  90. flowfile/web/static/assets/{dropDown-3d8dc5fa.css → dropDown-1d6acbd9.css} +26 -26
  91. flowfile/web/static/assets/{dropDown-ac0fda9d.js → dropDown-2798a109.js} +3 -3
  92. flowfile/web/static/assets/{fullEditor-5497a84a.js → fullEditor-cf7d7d93.js} +3 -3
  93. flowfile/web/static/assets/{fullEditor-a0be62b3.css → fullEditor-fe9f7e18.css} +3 -3
  94. flowfile/web/static/assets/{genericNodeSettings-99014e1d.js → genericNodeSettings-14eac1c3.js} +3 -3
  95. flowfile/web/static/assets/{index-fb6493ae.js → index-387a6f18.js} +175 -86
  96. flowfile/web/static/assets/{index-07dda503.js → index-6b367bb5.js} +1 -1
  97. flowfile/web/static/assets/{index-e6289dd0.css → index-e96ab018.css} +249 -10
  98. flowfile/web/static/assets/{index-3ba44389.js → index-f0a6e5a5.js} +2 -2
  99. flowfile/web/static/assets/nodeInput-ed2ae8d7.js +2 -0
  100. flowfile/web/static/assets/{outputCsv-8f8ba42d.js → outputCsv-3c1757e8.js} +1 -1
  101. flowfile/web/static/assets/{outputExcel-393f4fef.js → outputExcel-686e1f48.js} +1 -1
  102. flowfile/web/static/assets/{outputParquet-07c81f65.js → outputParquet-df28faa7.js} +1 -1
  103. flowfile/web/static/assets/{readCsv-07f6d9ad.js → readCsv-e37eee21.js} +1 -1
  104. flowfile/web/static/assets/{readExcel-ed69bc8f.js → readExcel-a13f14bb.js} +3 -3
  105. flowfile/web/static/assets/{readParquet-e3ed4528.js → readParquet-344cf746.js} +1 -1
  106. flowfile/web/static/assets/{secrets.api-002e7d7e.js → secrets.api-ae198c5c.js} +1 -1
  107. flowfile/web/static/assets/{selectDynamic-80b92899.js → selectDynamic-6b4b0767.js} +3 -3
  108. flowfile/web/static/assets/{vue-codemirror.esm-0965f39f.js → vue-codemirror.esm-31ba0e0b.js} +1 -1
  109. flowfile/web/static/assets/{vue-content-loader.es-c506ad97.js → vue-content-loader.es-4469c8ff.js} +1 -1
  110. flowfile/web/static/index.html +2 -2
  111. {flowfile-0.5.3.dist-info → flowfile-0.5.4.dist-info}/METADATA +2 -2
  112. {flowfile-0.5.3.dist-info → flowfile-0.5.4.dist-info}/RECORD +134 -129
  113. flowfile_core/auth/secrets.py +56 -13
  114. flowfile_core/fileExplorer/funcs.py +26 -4
  115. flowfile_core/flowfile/code_generator/__init__.py +11 -0
  116. flowfile_core/flowfile/code_generator/code_generator.py +347 -2
  117. flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +13 -1
  118. flowfile_core/flowfile/flow_graph.py +2 -0
  119. flowfile_core/flowfile/flow_node/flow_node.py +52 -28
  120. flowfile_core/flowfile/node_designer/__init__.py +4 -0
  121. flowfile_core/flowfile/node_designer/ui_components.py +144 -1
  122. flowfile_core/routes/public.py +43 -1
  123. flowfile_core/schemas/cloud_storage_schemas.py +39 -15
  124. flowfile_core/secret_manager/secret_manager.py +107 -6
  125. flowfile_frame/__init__.py +11 -0
  126. flowfile_frame/database/__init__.py +36 -0
  127. flowfile_frame/database/connection_manager.py +205 -0
  128. flowfile_frame/database/frame_helpers.py +249 -0
  129. flowfile_worker/configs.py +31 -15
  130. flowfile_worker/secrets.py +105 -15
  131. flowfile_worker/spawner.py +10 -6
  132. flowfile/web/static/assets/ContextMenu-26d4dd27.css +0 -26
  133. flowfile/web/static/assets/ContextMenu-31ee57f0.js +0 -41
  134. flowfile/web/static/assets/ContextMenu-69a74055.js +0 -41
  135. flowfile/web/static/assets/ContextMenu-8e2051c6.js +0 -41
  136. flowfile/web/static/assets/ContextMenu-8ec1729e.css +0 -26
  137. flowfile/web/static/assets/ContextMenu-9b310c60.css +0 -26
  138. flowfile/web/static/assets/CustomNode-59e99a86.css +0 -32
  139. flowfile/web/static/assets/GroupBy-be7ac0bf.css +0 -51
  140. flowfile/web/static/assets/SQLQueryComponent-1c2f26b4.css +0 -27
  141. flowfile/web/static/assets/Sort-8a871341.css +0 -51
  142. flowfile/web/static/assets/Unique-9fb2f567.css +0 -51
  143. flowfile/web/static/assets/nodeInput-0eb13f1a.js +0 -2
  144. {flowfile-0.5.3.dist-info → flowfile-0.5.4.dist-info}/WHEEL +0 -0
  145. {flowfile-0.5.3.dist-info → flowfile-0.5.4.dist-info}/entry_points.txt +0 -0
  146. {flowfile-0.5.3.dist-info → flowfile-0.5.4.dist-info}/licenses/LICENSE +0 -0
@@ -15,7 +15,9 @@ from .custom_node import CustomNodeBase, NodeSettings
15
15
 
16
16
  # Import all UI components so they can be used directly
17
17
  from .ui_components import (
18
+ ActionOption,
18
19
  AvailableSecrets,
20
+ ColumnActionInput,
19
21
  ColumnSelector,
20
22
  IncomingColumns,
21
23
  MultiSelect,
@@ -42,6 +44,8 @@ __all__ = [
42
44
  "MultiSelect",
43
45
  "NodeSettings",
44
46
  "ColumnSelector",
47
+ "ColumnActionInput",
48
+ "ActionOption",
45
49
  "IncomingColumns",
46
50
  "AvailableSecrets",
47
51
  "SecretSelector",
@@ -1,4 +1,4 @@
1
- from typing import Any, Literal
1
+ from typing import Any, Literal, NamedTuple
2
2
 
3
3
  from pydantic import BaseModel, Field, SecretStr, computed_field
4
4
 
@@ -11,6 +11,29 @@ from flowfile_core.types import DataType, TypeSpec
11
11
  InputType = Literal["text", "number", "secret", "array", "date", "boolean"]
12
12
 
13
13
 
14
+ class ActionOption(NamedTuple):
15
+ """
16
+ A named tuple representing an action option with a value and display label.
17
+
18
+ Use this to define actions with custom labels in ColumnActionInput:
19
+ actions=[
20
+ ActionOption("sum", "Sum"),
21
+ ActionOption("avg", "Average"),
22
+ "count" # plain strings also work
23
+ ]
24
+ """
25
+
26
+ value: str
27
+ """The internal value used in the data."""
28
+
29
+ label: str
30
+ """The display label shown in the UI."""
31
+
32
+
33
+ # Type alias for action specifications - accepts strings or ActionOption tuples
34
+ ActionSpec = str | ActionOption
35
+
36
+
14
37
  def normalize_input_to_data_types(v: Any) -> Literal["ALL"] | list[DataType]:
15
38
  """
16
39
  Normalizes a wide variety of inputs to either 'ALL' or a sorted list of DataType enums.
@@ -376,3 +399,123 @@ class SecretSelector(FlowfileInComponent):
376
399
  if self.name_prefix:
377
400
  data["name_prefix"] = self.name_prefix
378
401
  return data
402
+
403
+
404
+ class ColumnActionInput(FlowfileInComponent):
405
+ """
406
+ A generic UI component for configuring column-based transformations.
407
+
408
+ This component allows users to select columns, choose an action/transformation,
409
+ and optionally rename the output. It can be configured for many use cases:
410
+ rolling windows, aggregations, string transformations, type conversions, etc.
411
+
412
+ The component displays:
413
+ - A list of available columns (filterable by data type)
414
+ - A table of configured operations with: Column, Action, Output Name
415
+ - Optional group by and order by selectors
416
+
417
+ Example - Rolling Window:
418
+ ColumnActionInput(
419
+ label="Rolling Calculations",
420
+ actions=["sum", "mean", "min", "max"],
421
+ output_name_template="{column}_rolling_{action}",
422
+ show_group_by=True,
423
+ show_order_by=True,
424
+ data_types="Numeric"
425
+ )
426
+
427
+ Example - String Transformations:
428
+ ColumnActionInput(
429
+ label="String Operations",
430
+ actions=["upper", "lower", "trim", "reverse"],
431
+ output_name_template="{column}_{action}",
432
+ data_types="String"
433
+ )
434
+
435
+ Example - Aggregations with ActionOption:
436
+ ColumnActionInput(
437
+ label="Aggregations",
438
+ actions=[
439
+ ActionOption("sum", "Sum"),
440
+ ActionOption("count", "Count"),
441
+ ActionOption("mean", "Average"),
442
+ "min", # plain strings also work
443
+ ],
444
+ output_name_template="{column}_{action}",
445
+ show_group_by=True
446
+ )
447
+ """
448
+
449
+ component_type: Literal["ColumnActionInput"] = "ColumnActionInput"
450
+ input_type: InputType = "array"
451
+
452
+ # Configurable actions - list of action names or ActionOption tuples
453
+ actions: list[ActionSpec] = Field(default_factory=list)
454
+ """Actions available for selection. Can be strings or ActionOption(value, label) tuples."""
455
+
456
+ # Template for auto-generating output names
457
+ # Supports placeholders: {column}, {action}
458
+ output_name_template: str = "{column}_{action}"
459
+ """Template for generating default output names. Use {column} and {action} placeholders."""
460
+
461
+ # Optional grouping/ordering support
462
+ show_group_by: bool = False
463
+ """Whether to show the group by column selector."""
464
+
465
+ show_order_by: bool = False
466
+ """Whether to show the order by column selector."""
467
+
468
+ # Type filtering for column selection
469
+ data_type_filter_input: TypeSpec = Field(default="ALL", alias="data_types", repr=False, exclude=True)
470
+ """Filter columns by data type. Defaults to ALL."""
471
+
472
+ class Config:
473
+ arbitrary_types_allowed = True
474
+
475
+ def __init__(self, **data):
476
+ super().__init__(**data)
477
+ # Initialize value if not set
478
+ if self.value is None:
479
+ self.value = {
480
+ "rows": [],
481
+ "group_by_columns": [],
482
+ "order_by_column": None,
483
+ }
484
+
485
+ def set_value(self, value: Any):
486
+ """
487
+ Sets the value from frontend.
488
+ """
489
+ self.value = value
490
+ return self
491
+
492
+ @computed_field
493
+ @property
494
+ def data_types_filter(self) -> Literal["ALL"] | list[DataType]:
495
+ """
496
+ A computed field that normalizes the `data_type_filter_input` into a
497
+ standardized format for the frontend.
498
+ """
499
+ return normalize_input_to_data_types(self.data_type_filter_input)
500
+
501
+ def model_dump(self, **kwargs) -> dict:
502
+ """
503
+ Serializes the component for the frontend.
504
+ """
505
+ data = super().model_dump(**kwargs)
506
+ # Normalize actions to list of {value, label} objects for frontend
507
+ normalized_actions = []
508
+ for action in self.actions:
509
+ if isinstance(action, tuple):
510
+ normalized_actions.append({"value": action[0], "label": action[1]})
511
+ else:
512
+ normalized_actions.append({"value": action, "label": action})
513
+ data["actions"] = normalized_actions
514
+ data["output_name_template"] = self.output_name_template
515
+ data["show_group_by"] = self.show_group_by
516
+ data["show_order_by"] = self.show_order_by
517
+ if "data_types_filter" in data and data["data_types_filter"] != "ALL":
518
+ data["data_types"] = sorted([dt.value for dt in data["data_types_filter"]])
519
+ else:
520
+ data["data_types"] = "ALL"
521
+ return data
@@ -1,11 +1,53 @@
1
+ import os
2
+
1
3
  from fastapi import APIRouter
2
4
  from fastapi.responses import RedirectResponse
5
+ from pydantic import BaseModel
6
+
7
+ from flowfile_core.auth.secrets import generate_master_key, is_master_key_configured
3
8
 
4
- # Router setup
5
9
  router = APIRouter()
6
10
 
7
11
 
12
+ class SetupStatus(BaseModel):
13
+ """Response model for the setup status endpoint."""
14
+
15
+ setup_required: bool
16
+ master_key_configured: bool
17
+ mode: str
18
+
19
+
20
+ class GeneratedKey(BaseModel):
21
+ """Response model for the generate key endpoint."""
22
+
23
+ key: str
24
+ instructions: str
25
+
26
+
8
27
  @router.get("/", tags=["admin"])
9
28
  async def docs_redirect():
10
29
  """Redirects to the documentation page."""
11
30
  return RedirectResponse(url="/docs")
31
+
32
+
33
+ @router.get("/health/status", response_model=SetupStatus, tags=["health"])
34
+ async def get_setup_status():
35
+ """Get the current setup status of the application."""
36
+ mode = os.environ.get("FLOWFILE_MODE", "electron")
37
+ master_key_ok = is_master_key_configured()
38
+ return SetupStatus(
39
+ setup_required=not master_key_ok,
40
+ master_key_configured=master_key_ok,
41
+ mode=mode,
42
+ )
43
+
44
+
45
+ @router.post("/setup/generate-key", response_model=GeneratedKey, tags=["setup"])
46
+ async def generate_key():
47
+ """Generate a new master encryption key."""
48
+ key = generate_master_key()
49
+ instructions = (
50
+ f'Add to your .env file:\n FLOWFILE_MASTER_KEY="{key}"\n\n'
51
+ "Then restart: docker-compose down && docker-compose up"
52
+ )
53
+ return GeneratedKey(key=key, instructions=instructions)
@@ -14,14 +14,20 @@ AuthMethod = Literal[
14
14
  ]
15
15
 
16
16
 
17
- def encrypt_for_worker(secret_value: SecretStr | None) -> str | None:
17
+ def encrypt_for_worker(secret_value: SecretStr | None, user_id: int) -> str | None:
18
18
  """
19
- Encrypts a secret value for use in worker contexts.
20
- This is a placeholder function that simulates encryption.
21
- In practice, you would use a secure encryption method.
19
+ Encrypts a secret value for use in worker contexts using per-user key derivation.
20
+
21
+ Args:
22
+ secret_value: The secret value to encrypt
23
+ user_id: The user ID for key derivation
24
+
25
+ Returns:
26
+ Encrypted secret with embedded user_id, or None if secret_value is None
22
27
  """
23
28
  if secret_value is not None:
24
- return encrypt_secret(secret_value.get_secret_value())
29
+ return encrypt_secret(secret_value.get_secret_value(), user_id)
30
+ return None
25
31
 
26
32
 
27
33
  class AuthSettingsInput(BaseModel):
@@ -80,25 +86,31 @@ class FullCloudStorageConnection(AuthSettingsInput):
80
86
  endpoint_url: str | None = None
81
87
  verify_ssl: bool = True
82
88
 
83
- def get_worker_interface(self) -> "FullCloudStorageConnectionWorkerInterface":
89
+ def get_worker_interface(self, user_id: int) -> "FullCloudStorageConnectionWorkerInterface":
84
90
  """
85
- Convert to a public interface model without secrets.
91
+ Convert to a worker interface model with encrypted secrets.
92
+
93
+ Args:
94
+ user_id: The user ID for per-user key derivation
95
+
96
+ Returns:
97
+ FullCloudStorageConnectionWorkerInterface with encrypted secrets
86
98
  """
87
99
  return FullCloudStorageConnectionWorkerInterface(
88
100
  storage_type=self.storage_type,
89
101
  auth_method=self.auth_method,
90
102
  connection_name=self.connection_name,
91
103
  aws_allow_unsafe_html=self.aws_allow_unsafe_html,
92
- aws_secret_access_key=encrypt_for_worker(self.aws_secret_access_key),
104
+ aws_secret_access_key=encrypt_for_worker(self.aws_secret_access_key, user_id),
93
105
  aws_region=self.aws_region,
94
106
  aws_access_key_id=self.aws_access_key_id,
95
107
  aws_role_arn=self.aws_role_arn,
96
- aws_session_token=encrypt_for_worker(self.aws_session_token),
108
+ aws_session_token=encrypt_for_worker(self.aws_session_token, user_id),
97
109
  azure_account_name=self.azure_account_name,
98
110
  azure_tenant_id=self.azure_tenant_id,
99
- azure_account_key=encrypt_for_worker(self.azure_account_key),
111
+ azure_account_key=encrypt_for_worker(self.azure_account_key, user_id),
100
112
  azure_client_id=self.azure_client_id,
101
- azure_client_secret=encrypt_for_worker(self.azure_client_secret),
113
+ azure_client_secret=encrypt_for_worker(self.azure_client_secret, user_id),
102
114
  endpoint_url=self.endpoint_url,
103
115
  verify_ssl=self.verify_ssl,
104
116
  )
@@ -202,18 +214,30 @@ def get_cloud_storage_write_settings_worker_interface(
202
214
  write_settings: CloudStorageWriteSettings,
203
215
  connection: FullCloudStorageConnection,
204
216
  lf: pl.LazyFrame,
217
+ user_id: int,
205
218
  flowfile_flow_id: int = 1,
206
219
  flowfile_node_id: int | str = -1,
207
220
  ) -> CloudStorageWriteSettingsWorkerInterface:
208
221
  """
209
- Convert to a worker interface model with hashed secrets.
222
+ Convert to a worker interface model with encrypted secrets.
223
+
224
+ Args:
225
+ write_settings: Cloud storage write settings
226
+ connection: Full cloud storage connection with secrets
227
+ lf: LazyFrame to serialize
228
+ user_id: User ID for per-user key derivation
229
+ flowfile_flow_id: Flow ID for tracking
230
+ flowfile_node_id: Node ID for tracking
231
+
232
+ Returns:
233
+ CloudStorageWriteSettingsWorkerInterface ready for worker
210
234
  """
211
235
  operation = base64.b64encode(lf.serialize()).decode()
212
236
 
213
237
  return CloudStorageWriteSettingsWorkerInterface(
214
238
  operation=operation,
215
239
  write_settings=write_settings.get_write_setting_worker_interface(),
216
- connection=connection.get_worker_interface(),
217
- flowfile_flow_id=flowfile_flow_id, # Default value, can be overridden
218
- flowfile_node_id=flowfile_node_id, # Default value, can be overridden
240
+ connection=connection.get_worker_interface(user_id),
241
+ flowfile_flow_id=flowfile_flow_id,
242
+ flowfile_node_id=flowfile_node_id,
219
243
  )
@@ -1,4 +1,8 @@
1
+ import base64
2
+
1
3
  from cryptography.fernet import Fernet
4
+ from cryptography.hazmat.primitives import hashes
5
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
2
6
  from fastapi.exceptions import HTTPException
3
7
  from pydantic import SecretStr
4
8
  from sqlalchemy import and_
@@ -9,21 +13,118 @@ from flowfile_core.auth.secrets import get_master_key
9
13
  from flowfile_core.database import models as db_models
10
14
  from flowfile_core.database.connection import get_db_context
11
15
 
16
+ # Version identifier for key derivation scheme (allows future migrations)
17
+ KEY_DERIVATION_VERSION = b"flowfile-secrets-v1"
18
+
19
+ # Encrypted secret format: $ffsec$1${user_id}${fernet_token}
20
+ # This embeds the user_id so the worker can derive the correct key
21
+ SECRET_FORMAT_PREFIX = "$ffsec$1$"
22
+
23
+
24
+ def derive_user_key(user_id: int) -> bytes:
25
+ """
26
+ Derive a user-specific encryption key from the master key using HKDF.
27
+
28
+ This provides cryptographic isolation between users - each user's secrets
29
+ are encrypted with a unique key derived from the master key.
30
+
31
+ Args:
32
+ user_id: The unique identifier for the user
33
+
34
+ Returns:
35
+ bytes: A 32-byte URL-safe base64-encoded key suitable for Fernet
36
+ """
37
+ master_key = get_master_key().encode()
12
38
 
13
- def encrypt_secret(secret_value):
14
- """Encrypt a secret value using the master key."""
39
+ # Use HKDF to derive a user-specific key
40
+ hkdf = HKDF(
41
+ algorithm=hashes.SHA256(),
42
+ length=32, # Fernet requires 32 bytes
43
+ salt=KEY_DERIVATION_VERSION, # Static salt is fine for key derivation
44
+ info=f"user-{user_id}".encode(), # User-specific context
45
+ )
46
+
47
+ # Derive raw key material and encode for Fernet
48
+ derived_key = hkdf.derive(master_key)
49
+ return base64.urlsafe_b64encode(derived_key)
50
+
51
+
52
+ def _encrypt_with_master_key(secret_value: str) -> str:
53
+ """Legacy encryption using master key directly (for backward compatibility)."""
15
54
  key = get_master_key().encode()
16
55
  f = Fernet(key)
17
56
  return f.encrypt(secret_value.encode()).decode()
18
57
 
19
58
 
20
- def decrypt_secret(encrypted_value) -> SecretStr:
21
- """Decrypt an encrypted value using the master key."""
59
+ def _decrypt_with_master_key(encrypted_value: str) -> SecretStr:
60
+ """Legacy decryption using master key directly (for backward compatibility)."""
22
61
  key = get_master_key().encode()
23
62
  f = Fernet(key)
24
63
  return SecretStr(f.decrypt(encrypted_value.encode()).decode())
25
64
 
26
65
 
66
+ def encrypt_secret(secret_value: str, user_id: int) -> str:
67
+ """
68
+ Encrypt a secret value using a user-specific derived key.
69
+
70
+ The encrypted format embeds the user_id so it can be decrypted
71
+ without knowing the user context (e.g., by the worker service).
72
+
73
+ Format: $ffsec$1${user_id}${fernet_token}
74
+
75
+ Args:
76
+ secret_value: The plaintext secret to encrypt
77
+ user_id: The user ID to derive the encryption key for
78
+
79
+ Returns:
80
+ str: The encrypted value with embedded user_id
81
+ """
82
+ key = derive_user_key(user_id)
83
+ f = Fernet(key)
84
+ fernet_token = f.encrypt(secret_value.encode()).decode()
85
+ return f"{SECRET_FORMAT_PREFIX}{user_id}${fernet_token}"
86
+
87
+
88
+ def decrypt_secret(encrypted_value: str, user_id: int | None = None) -> SecretStr:
89
+ """
90
+ Decrypt an encrypted value.
91
+
92
+ Supports both new format (with embedded user_id) and legacy format.
93
+ - New format: $ffsec$1${user_id}${fernet_token} - user_id extracted automatically
94
+ - Legacy format: raw Fernet token - requires user_id parameter or uses master key
95
+
96
+ Args:
97
+ encrypted_value: The encrypted secret
98
+ user_id: Optional user ID (required for legacy secrets, ignored for new format)
99
+
100
+ Returns:
101
+ SecretStr: The decrypted value wrapped in SecretStr
102
+ """
103
+ # Check for new versioned format with embedded user_id
104
+ if encrypted_value.startswith(SECRET_FORMAT_PREFIX):
105
+ # Parse: $ffsec$1${user_id}${fernet_token}
106
+ remainder = encrypted_value[len(SECRET_FORMAT_PREFIX):]
107
+ parts = remainder.split("$", 1)
108
+ if len(parts) != 2:
109
+ raise ValueError("Invalid encrypted secret format")
110
+
111
+ embedded_user_id = int(parts[0])
112
+ fernet_token = parts[1]
113
+
114
+ key = derive_user_key(embedded_user_id)
115
+ f = Fernet(key)
116
+ return SecretStr(f.decrypt(fernet_token.encode()).decode())
117
+
118
+ # Legacy format - use provided user_id or fall back to master key
119
+ if user_id is not None:
120
+ key = derive_user_key(user_id)
121
+ f = Fernet(key)
122
+ return SecretStr(f.decrypt(encrypted_value.encode()).decode())
123
+
124
+ # Fall back to master key for legacy secrets without user context
125
+ return _decrypt_with_master_key(encrypted_value)
126
+
127
+
27
128
  def get_encrypted_secret(current_user_id: int, secret_name: str) -> str | None:
28
129
  with get_db_context() as db:
29
130
  user_id = current_user_id
@@ -39,13 +140,13 @@ def get_encrypted_secret(current_user_id: int, secret_name: str) -> str | None:
39
140
 
40
141
 
41
142
  def store_secret(db: Session, secret: SecretInput, user_id: int) -> db_models.Secret:
42
- encrypted_value = encrypt_secret(secret.value.get_secret_value())
143
+ encrypted_value = encrypt_secret(secret.value.get_secret_value(), user_id)
43
144
 
44
145
  # Store in database
45
146
  db_secret = db_models.Secret(
46
147
  name=secret.name,
47
148
  encrypted_value=encrypted_value,
48
- iv="", # Not used with Fernet
149
+ iv="", # Legacy field, not used with current encryption
49
150
  user_id=user_id,
50
151
  )
51
152
  db.add(db_secret)
@@ -54,6 +54,17 @@ from flowfile_frame.cloud_storage.secret_manager import (
54
54
  get_all_available_cloud_storage_connections,
55
55
  )
56
56
 
57
+ # Database I/O
58
+ from flowfile_frame.database import (
59
+ create_database_connection,
60
+ create_database_connection_if_not_exists,
61
+ del_database_connection,
62
+ get_all_available_database_connections,
63
+ get_database_connection_by_name,
64
+ read_database,
65
+ write_database,
66
+ )
67
+
57
68
  # Commonly used functions
58
69
  from flowfile_frame.expr import ( # noqa: F401
59
70
  col,
@@ -0,0 +1,36 @@
1
+ """Database module for flowfile_frame.
2
+
3
+ This module provides functions for:
4
+ - Managing database connections (create, list, delete)
5
+ - Reading from databases
6
+ - Writing to databases
7
+ """
8
+
9
+ from flowfile_frame.database.connection_manager import (
10
+ create_database_connection,
11
+ create_database_connection_if_not_exists,
12
+ del_database_connection,
13
+ get_all_available_database_connections,
14
+ get_database_connection_by_name,
15
+ )
16
+ from flowfile_frame.database.frame_helpers import (
17
+ add_read_from_database,
18
+ add_write_to_database,
19
+ read_database,
20
+ write_database,
21
+ )
22
+
23
+ __all__ = [
24
+ # Connection management
25
+ "create_database_connection",
26
+ "create_database_connection_if_not_exists",
27
+ "del_database_connection",
28
+ "get_all_available_database_connections",
29
+ "get_database_connection_by_name",
30
+ # FlowGraph helpers
31
+ "add_read_from_database",
32
+ "add_write_to_database",
33
+ # Direct read/write
34
+ "read_database",
35
+ "write_database",
36
+ ]