فهرست منبع

Support authorization for all controls (taipy-enterprise #233) (#322)

* Support authorization for all controls (taipy-enterprise #233)

* pycodestyle

* isort

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 1 سال پیش
والد
کامیت
5c99b1fe52
5فایلهای تغییر یافته به همراه155 افزوده شده و 49 حذف شده
  1. 20 14
      gui/src/DataNodeViewer.tsx
  2. 18 5
      gui/src/ScenarioViewer.tsx
  3. 17 16
      gui/src/utils.ts
  4. 5 1
      src/taipy/gui_core/_adapters.py
  5. 95 13
      src/taipy/gui_core/_context.py

+ 20 - 14
gui/src/DataNodeViewer.tsx

@@ -92,18 +92,20 @@ const editSx = {
 const textFieldProps = { textField: { margin: "dense" } } as BaseDateTimePickerSlotsComponentsProps<Date>;
 
 type DataNodeFull = [
-    string,
-    string,
-    string,
-    string,
-    string,
-    string,
-    string,
-    string,
-    number,
-    Array<[string, string]>,
-    boolean,
-    string
+    string,     // id
+    string,     // type
+    string,     // config_id
+    string,     // last_edit_date
+    string,     // expiration_date
+    string,     // label
+    string,     // ownerId
+    string,     // ownerLabel
+    number,     // ownerType
+    Array<[string, string]>,    // properties
+    boolean,    // editInProgress
+    string,     // editorId
+    boolean,    // readable
+    boolean,    // editable
 ];
 
 enum DataNodeFullProps {
@@ -119,6 +121,8 @@ enum DataNodeFullProps {
     properties,
     editInProgress,
     editorId,
+    readable,
+    editable
 }
 const DataNodeFullLength = Object.keys(DataNodeFullProps).length / 2;
 
@@ -212,6 +216,8 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         dnProperties,
         dnEditInProgress,
         dnEditorId,
+        dnReadable,
+        dnEditable,
         isDataNode,
     ] = useMemo(() => {
         let dn: DataNodeFull | undefined = undefined;
@@ -233,7 +239,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
             oldId.current = dn[DataNodeFullProps.id];
         }
         editLock.current = dn ? dn[DataNodeFullProps.editInProgress] : false;
-        return dn ? [...dn, true] : ["", "", "", "", "", "", "", "", -1, [], false, "", false];
+        return dn ? [...dn, true] : ["", "", "", "", "", "", "", "", -1, [], false, "", false, false, false];
     }, [props.dataNode, props.defaultDataNode, oldId, id, dispatch, module, props.onLock]);
 
     // clean lock on unmount
@@ -246,7 +252,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         [id, dispatch, module, props.onLock]
     );
 
-    const active = useDynamicProperty(props.active, props.defaultActive, true);
+    const active = useDynamicProperty(props.active, props.defaultActive, true) && dnEditable && dnReadable;
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
 
     // history & data

+ 18 - 5
gui/src/ScenarioViewer.tsx

@@ -231,6 +231,8 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         scDeletable,
         scPromotable,
         scSubmittable,
+        scReadable,
+        scEditable,
         isScenario,
     ] = useMemo(() => {
         let sc: ScenarioFull | undefined = undefined;
@@ -243,10 +245,10 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                 // DO nothing
             }
         }
-        return sc ? [...sc, true] : ["", false, "", "", "", [], [], [], [], false, false, false, false];
+        return sc ? [...sc, true] : ["", false, "", "", "", [], [], [], [], false, false, false, false, false, false];
     }, [props.scenario, props.defaultScenario]);
 
-    const active = useDynamicProperty(props.active, props.defaultActive, true);
+    const active = useDynamicProperty(props.active, props.defaultActive, true) && scEditable && scReadable;
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
 
     const [deleteDialog, setDeleteDialogOpen] = useState(false);
@@ -279,7 +281,13 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     // submits
     const submitSequence = useCallback(
         (sequenceId: string) => {
-            dispatch(createSendActionNameAction(id, module, props.onSubmit, { id: sequenceId, on_submission_change: props.onSubmissionChange }));
+            dispatch(
+                createSendActionNameAction(id, module, props.onSubmit, {
+                    id: sequenceId,
+                    on_submission_change: props.onSubmissionChange,
+                    type: "Sequence"
+                })
+            );
         },
         [props.onSubmit, props.onSubmissionChange, id, dispatch, module]
     );
@@ -288,7 +296,12 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         (e: React.MouseEvent<HTMLElement>) => {
             e.stopPropagation();
             if (isScenario) {
-                dispatch(createSendActionNameAction(id, module, props.onSubmit, { id: scId, on_submission_change: props.onSubmissionChange }));
+                dispatch(
+                    createSendActionNameAction(id, module, props.onSubmit, {
+                        id: scId,
+                        on_submission_change: props.onSubmissionChange,
+                    })
+                );
             }
         },
         [isScenario, props.onSubmit, props.onSubmissionChange, id, scId, dispatch, module]
@@ -349,7 +362,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     const editSequence = useCallback(
         (id: string, label: string) => {
             if (isScenario) {
-                dispatch(createSendActionNameAction(id, module, props.onEdit, { id: id, name: label }));
+                dispatch(createSendActionNameAction(id, module, props.onEdit, { id: id, name: label, type: "Sequence" }));
                 setFocusName("");
             }
         },

+ 17 - 16
gui/src/utils.ts

@@ -15,20 +15,21 @@ import { PopoverOrigin } from "@mui/material/Popover";
 
 import { useDynamicProperty } from "taipy-gui";
 
-// id, is_primary, config_id, creation_date, label, tags, properties(key, value), sequences(id, label), authorized_tags, deletable
 export type ScenarioFull = [
-    string,
-    boolean,
-    string,
-    string,
-    string,
-    string[],
-    Array<[string, string]>,
-    Array<[string, string, boolean]>,
-    string[],
-    boolean,
-    boolean,
-    boolean
+    string,     // id
+    boolean,    // is_primary
+    string,     // config_id
+    string,     // creation_date
+    string,     // label
+    string[],   // tags
+    Array<[string, string]>,    // properties
+    Array<[string, string, boolean]>,   // sequences
+    string[],   // authorized_tags
+    boolean,    // deletable
+    boolean,    // promotable
+    boolean,    // submittable
+    boolean,    // readable
+    boolean     // editable
 ];
 
 export enum ScFProps {
@@ -44,6 +45,8 @@ export enum ScFProps {
     deletable,
     promotable,
     submittable,
+    readable,
+    editable,
 }
 export const ScenarioFullLength = Object.keys(ScFProps).length / 2;
 
@@ -148,8 +151,7 @@ export const FieldNoMaxWidth = {
 
 export const IconPaddingSx = { padding: 0 };
 
-export const MainBoxSx = {
-};
+export const MainBoxSx = {};
 
 export const AccordionIconSx = { fontSize: "0.9rem" };
 
@@ -207,4 +209,3 @@ export const MenuProps = {
     },
 };
 export const selectSx = { m: 1, width: 300 };
-

+ 5 - 1
src/taipy/gui_core/_adapters.py

@@ -14,7 +14,7 @@ from enum import Enum
 
 from taipy.core import Cycle, DataNode, Job, Scenario, Sequence
 from taipy.core import get as core_get
-from taipy.core import is_deletable, is_promotable, is_submittable
+from taipy.core import is_deletable, is_editable, is_promotable, is_readable, is_submittable
 from taipy.gui.gui import _DoNotUpdate
 from taipy.gui.utils import _TaipyBase
 
@@ -63,6 +63,8 @@ class _GuiCoreScenarioAdapter(_TaipyBase):
                     is_deletable(scenario),
                     is_promotable(scenario),
                     is_submittable(scenario),
+                    is_readable(scenario),
+                    is_editable(scenario),
                 ]
         return None
 
@@ -143,6 +145,8 @@ class _GuiCoreDatanodeAdapter(_TaipyBase):
                     ],
                     datanode._edit_in_progress,
                     datanode._editor_id,
+                    is_readable(datanode),
+                    is_editable(datanode),
                 ]
         return None
 

+ 95 - 13
src/taipy/gui_core/_context.py

@@ -29,11 +29,21 @@ from taipy.core import Cycle, DataNode, Job, Scenario, Sequence, cancel_job, cre
 from taipy.core import delete as core_delete
 from taipy.core import delete_job
 from taipy.core import get as core_get
-from taipy.core import get_cycles_scenarios, get_data_nodes, get_jobs, set_primary
+from taipy.core import (
+    get_cycles_scenarios,
+    get_data_nodes,
+    get_jobs,
+    is_deletable,
+    is_editable,
+    is_promotable,
+    is_readable,
+    is_submittable,
+    set_primary,
+)
 from taipy.core import submit as core_submit
 from taipy.core.data._abstract_tabular import _AbstractTabularDataNode
 from taipy.core.notification import CoreEventConsumerBase, EventEntityType
-from taipy.core.notification.event import Event
+from taipy.core.notification.event import Event, EventOperation
 from taipy.core.notification.notifier import Notifier
 from taipy.gui import Gui, State
 from taipy.gui._warnings import _warn
@@ -117,13 +127,21 @@ class _GuiCoreContext(CoreEventConsumerBase):
             with self.lock:
                 self.scenario_by_cycle = None
                 self.data_nodes_by_owner = None
-            scenario = core_get(event.entity_id) if event.operation.value != 3 else None
+            scenario = (
+                core_get(event.entity_id)
+                if event.operation.value != EventOperation.DELETION and is_readable(event.entity_id)
+                else None
+            )
             self.gui._broadcast(
                 _GuiCoreContext._CORE_CHANGED_NAME,
                 {"scenario": event.entity_id if scenario else True},
             )
-        elif event.entity_type == EventEntityType.SEQUENCE and event.entity_id:  # TODO import EventOperation
-            sequence = core_get(event.entity_id) if event.operation.value != 3 else None
+        elif event.entity_type == EventEntityType.SEQUENCE and event.entity_id:
+            sequence = (
+                core_get(event.entity_id)
+                if event.operation.value != EventOperation.DELETION and is_readable(event.entity_id)
+                else None
+            )
             if sequence:
                 if hasattr(sequence, "parent_ids") and sequence.parent_ids:
                     self.gui._broadcast(
@@ -132,19 +150,18 @@ class _GuiCoreContext(CoreEventConsumerBase):
         elif event.entity_type == EventEntityType.JOB:
             with self.lock:
                 self.jobs_list = None
-            if event.entity_id:
-                self.scenario_status_callback(event.entity_id)
+            self.scenario_status_callback(event.entity_id)
             self.gui._broadcast(_GuiCoreContext._CORE_CHANGED_NAME, {"jobs": True})
         elif event.entity_type == EventEntityType.DATA_NODE:
             with self.lock:
                 self.data_nodes_by_owner = None
             self.gui._broadcast(
                 _GuiCoreContext._CORE_CHANGED_NAME,
-                {"datanode": event.entity_id if event.operation.value != 3 else True},
+                {"datanode": event.entity_id if event.operation.value != EventOperation.DELETION else True},
             )
 
     def scenario_adapter(self, data):
-        if hasattr(data, "id") and core_get(data.id) is not None:
+        if hasattr(data, "id") and is_readable(data.id) and core_get(data.id) is not None:
             if self.scenario_by_cycle and isinstance(data, Cycle):
                 return (
                     data.id,
@@ -176,7 +193,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ID_VAR, args[0])
 
     def get_scenario_by_id(self, id: str) -> t.Optional[Scenario]:
-        if not id:
+        if not id or not is_readable(id):
             return None
         try:
             return core_get(id)
@@ -211,11 +228,20 @@ class _GuiCoreContext(CoreEventConsumerBase):
         if update:
             scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
             if delete:
+                if not is_deletable(scenario_id):
+                    state.assign(
+                        _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Scenario. {scenario_id} is not deletable."
+                    )
+                    return
                 try:
                     core_delete(scenario_id)
                 except Exception as e:
                     state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error deleting Scenario. {e}")
             else:
+                if not self.__check_readable_editable(
+                    state, scenario_id, "Scenario", _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR
+                ):
+                    return
                 scenario = core_get(scenario_id)
         else:
             config_id = data.get(_GuiCoreContext.__PROP_CONFIG_ID)
@@ -267,6 +293,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
             except Exception as e:
                 state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error creating Scenario. {e}")
         if scenario:
+            if not is_editable(scenario):
+                state.assign(
+                    _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Scenario {scenario_id or name} is not editable."
+                )
+                return
             with scenario as sc:
                 sc.properties[_GuiCoreContext.__PROP_ENTITY_NAME] = name
                 if props := data.get("properties"):
@@ -289,12 +320,21 @@ class _GuiCoreContext(CoreEventConsumerBase):
             return
         data = args[0]
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
+        if not self.__check_readable_editable(
+            state, entity_id, data.get("type", "Scenario"), _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR
+        ):
+            return
         entity: t.Union[Scenario, Sequence] = core_get(entity_id)
         if entity:
             try:
                 if isinstance(entity, Scenario):
                     primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY)
                     if primary is True:
+                        if not is_promotable(entity):
+                            state.assign(
+                                _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Scenario {entity_id} is not promotable."
+                            )
+                            return
                         set_primary(entity)
                 self.__edit_properties(entity, data)
                 state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
@@ -307,6 +347,12 @@ class _GuiCoreContext(CoreEventConsumerBase):
             return
         data = args[0]
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
+        if not is_submittable(entity_id):
+            state.assign(
+                _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR,
+                f"{data.get('type', 'Scenario')} {entity_id} is not submitable.",
+            )
+            return
         entity = core_get(entity_id)
         if entity:
             try:
@@ -377,7 +423,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return _SubmissionStatus.UNDEFINED
 
     def scenario_status_callback(self, job_id: str):
-        if not job_id:
+        if not job_id or not is_readable(job_id):
             return
         try:
             job = core_get(job_id)
@@ -485,6 +531,9 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     data.submit_id,
                     data.creation_date,
                     data.status.value,
+                    is_deletable(data),
+                    is_readable(data),
+                    is_editable(data),
                 )
 
     def act_on_jobs(self, state: State, id: str, payload: t.Dict[str, str]):
@@ -498,12 +547,18 @@ class _GuiCoreContext(CoreEventConsumerBase):
             errs = []
             if job_action == "delete":
                 for job_id in job_ids:
+                    if not is_deletable(job_id):
+                        errs.append(f"Job {job_id} is not deletable.")
+                        continue
                     try:
                         delete_job(core_get(job_id))
                     except Exception as e:
                         errs.append(f"Error deleting job. {e}")
             elif job_action == "cancel":
                 for job_id in job_ids:
+                    if not is_editable(job_id):
+                        errs.append(f"Job {job_id} is not cancelable.")
+                        continue
                     try:
                         cancel_job(job_id)
                     except Exception as e:
@@ -516,6 +571,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
             return
         data = args[0]
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
+        if not self.__check_readable_editable(state, entity_id, "DataNode", _GuiCoreContext._DATANODE_VIZ_ERROR_VAR):
+            return
         entity: DataNode = core_get(entity_id)
         if isinstance(entity, DataNode):
             try:
@@ -530,6 +587,9 @@ class _GuiCoreContext(CoreEventConsumerBase):
             return
         data = args[0]
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
+        if not is_editable(entity_id):
+            state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Datanode {entity_id} is not editable.")
+            return
         lock = data.get("lock", True)
         entity: DataNode = core_get(entity_id)
         if isinstance(entity, DataNode):
@@ -594,11 +654,17 @@ class _GuiCoreContext(CoreEventConsumerBase):
         ):
             res = []
             for e in dn.edits:
-                job: Job = core_get(e.get("job_id")) if "job_id" in e else None
+                job_id = e.get("job_id")
+                job: Job = None
+                if job_id:
+                    if not is_readable(job_id):
+                        job_id += " not readable"
+                    else:
+                        job = core_get(job_id)
                 res.append(
                     (
                         e.get("timestamp"),
-                        job.id if job else e.get("writer_identifier", ""),
+                        job_id if job_id else e.get("writer_identifier", ""),
                         f"Execution of task {job.task.get_simple_label()}."
                         if job and job.task
                         else e.get("comment", ""),
@@ -637,12 +703,23 @@ class _GuiCoreContext(CoreEventConsumerBase):
             return (None, None, None, f"Data unavailable for {dn.get_simple_label()}")
         return _DoNotUpdate()
 
+    def __check_readable_editable(self, state: State, id: str, type: str, var: str):
+        if not is_readable(id):
+            state.assign(var, f"{type} {id} is not readable.")
+            return False
+        if not is_editable(id):
+            state.assign(var, f"{type} {id} is not editable.")
+            return False
+        return True
+
     def update_data(self, state: State, id: str, payload: t.Dict[str, str]):
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
         data = args[0]
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
+        if not self.__check_readable_editable(state, entity_id, "DataNode", _GuiCoreContext._DATANODE_VIZ_ERROR_VAR):
+            return
         entity: DataNode = core_get(entity_id)
         if isinstance(entity, DataNode):
             try:
@@ -665,6 +742,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
     def tabular_data_edit(self, state: State, var_name: str, payload: dict):
         user_data = payload.get("user_data", {})
         dn_id = user_data.get("dn_id")
+        if not self.__check_readable_editable(state, dn_id, "DataNode", _GuiCoreContext._DATANODE_VIZ_ERROR_VAR):
+            return
         datanode = core_get(dn_id) if dn_id else None
         if isinstance(datanode, DataNode):
             try:
@@ -693,6 +772,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             id
             and isinstance(datanode, DataNode)
             and id == datanode.id
+            and is_readable(id)
             and (dn := core_get(id))
             and isinstance(dn, DataNode)
             and dn.is_ready_for_reading
@@ -708,6 +788,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             id
             and isinstance(datanode, DataNode)
             and id == datanode.id
+            and is_readable(id)
             and (dn := core_get(id))
             and isinstance(dn, DataNode)
             and dn.is_ready_for_reading
@@ -725,6 +806,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             id
             and isinstance(datanode, DataNode)
             and id == datanode.id
+            and is_readable(id)
             and (dn := core_get(id))
             and isinstance(dn, DataNode)
             and dn.is_ready_for_reading