Jelajahi Sumber

Merge pull request #854 from porter-dev/0.6.0-implement-rbac-hooks-and-hoc

[0.6.0] Authorization module
Nicolas Frati 4 tahun lalu
induk
melakukan
b66ea8cc7c

+ 43 - 0
dashboard/src/shared/auth/AuthContext.tsx

@@ -0,0 +1,43 @@
+import React, { useContext, useEffect, useState } from "react";
+import { Context } from "shared/Context";
+import {
+  ADMIN_POLICY_MOCK,
+  POLICY_HIERARCHY_TREE,
+  populatePolicy,
+} from "./authorization-helpers";
+import { PolicyDocType } from "./types";
+
+type AuthContext = {
+  currentPolicy: PolicyDocType;
+};
+
+export const AuthContext = React.createContext<AuthContext>({} as AuthContext);
+
+const AuthProvider: React.FC = ({ children }) => {
+  const { user } = useContext(Context);
+  const [currentPolicy, setCurrentPolicy] = useState(null);
+
+  useEffect(() => {
+    if (!user) {
+      setCurrentPolicy(null);
+    } else {
+      // TODO: GET POLICY FROM ENDPOINT
+      setCurrentPolicy(
+        populatePolicy(
+          ADMIN_POLICY_MOCK,
+          POLICY_HIERARCHY_TREE,
+          ADMIN_POLICY_MOCK.scope,
+          ADMIN_POLICY_MOCK.verbs
+        )
+      );
+    }
+  }, [user]);
+
+  return (
+    <AuthContext.Provider value={{ currentPolicy }}>
+      {children}
+    </AuthContext.Provider>
+  );
+};
+
+export default AuthProvider;

+ 19 - 0
dashboard/src/shared/auth/AuthorizationHoc.tsx

@@ -0,0 +1,19 @@
+import React from "react";
+import { useContext } from "react";
+import { AuthContext } from "./AuthContext";
+import { isAuthorized } from "./authorization-helpers";
+import { ScopeType, Verbs } from "./types";
+
+export const withAuth = <ComponentProps extends object>(
+  scope: ScopeType,
+  resource: string,
+  verb: Verbs | Array<Verbs>
+) => (Component: any) => (props: ComponentProps) => {
+  const authContext = useContext(AuthContext);
+
+  if (isAuthorized(authContext.currentPolicy, scope, resource, verb)) {
+    return <Component {...props} />;
+  }
+
+  return null;
+};

+ 49 - 0
dashboard/src/shared/auth/RouteGuard.tsx

@@ -0,0 +1,49 @@
+import React, { useMemo, useContext } from "react";
+import { Redirect, Route, RouteProps } from "react-router";
+import { AuthContext } from "./AuthContext";
+import { isAuthorized } from "./authorization-helpers";
+import { ScopeType, Verbs } from "./types";
+
+type GuardedRouteProps = {
+  scope: ScopeType;
+  resource: string;
+  verb: Verbs | Array<Verbs>;
+};
+
+const GuardedRoute: React.FC<RouteProps & GuardedRouteProps> = ({
+  component: Component,
+  scope,
+  resource,
+  verb,
+  ...rest
+}) => {
+  const { currentPolicy } = useContext(AuthContext);
+  const auth = useMemo(() => {
+    return isAuthorized(currentPolicy, scope, resource, verb);
+  }, [currentPolicy, scope, resource, verb]);
+
+  return (
+    <Route
+      {...rest}
+      render={(props) =>
+        auth === true ? <Component {...props} /> : <Redirect to="/" />
+      }
+    />
+  );
+};
+
+export const fakeGuardedRoute = <ComponentProps extends object>(
+  scope: string,
+  resource: string,
+  verb: Verbs | Array<Verbs>
+) => (Component: any) => (props: ComponentProps) => {
+  const authContext = useContext(AuthContext);
+
+  if (isAuthorized(authContext.currentPolicy, scope, resource, verb)) {
+    return <Component {...props} />;
+  }
+
+  return <Redirect to="/" />;
+};
+
+export default GuardedRoute;

+ 88 - 0
dashboard/src/shared/auth/authorization-helpers.ts

@@ -0,0 +1,88 @@
+import { HIERARCHY_TREE, PolicyDocType, ScopeType, Verbs } from "./types";
+
+export const ADMIN_POLICY_MOCK: PolicyDocType = {
+  scope: "project",
+  verbs: ["get", "list", "create", "update", "delete"],
+  resources: [],
+  children: {
+    settings: {
+      scope: "settings",
+      verbs: [],
+    },
+  } as Record<ScopeType, PolicyDocType>,
+};
+
+export const isAuthorized = (
+  policy: PolicyDocType,
+  scope: string,
+  resource: string,
+  verb: Verbs | Array<Verbs>
+): boolean => {
+  if (!policy) {
+    return false;
+  }
+
+  if (policy?.scope === scope) {
+    return (policy.resources.length === 0 ||
+      policy.resources.includes(resource)) &&
+      typeof verb === "string"
+      ? policy.verbs.includes(verb)
+      : (verb as Array<Verbs>).every((v) => policy.verbs.includes(v));
+  } else {
+    const isValid =
+      policy?.children &&
+      Object.values(policy.children).reduce((prev, currentPol) => {
+        if (isAuthorized(currentPol, scope, resource, verb)) {
+          return true;
+        } else {
+          return prev || false;
+        }
+      }, false);
+
+    return !!isValid;
+  }
+};
+
+export const POLICY_HIERARCHY_TREE: HIERARCHY_TREE = {
+  project: {
+    cluster: {
+      namespace: {
+        application: {},
+      },
+    },
+    settings: {},
+  },
+};
+
+export const populatePolicy = (
+  currPolicy: PolicyDocType,
+  tree: HIERARCHY_TREE,
+  currScope: ScopeType,
+  parentVerbs: Array<Verbs>
+) => {
+  const currTree = tree[currScope];
+
+  const treeKeys = Object.keys(currTree) as Array<ScopeType>;
+
+  for (const child of treeKeys) {
+    let childPolicy = currPolicy?.children && currPolicy?.children[child];
+    if (!childPolicy) {
+      childPolicy = {
+        scope: child,
+        verbs: parentVerbs,
+        resources: [],
+        children: {},
+      };
+    }
+    childPolicy.resources = childPolicy?.resources || [];
+    childPolicy.children = childPolicy?.children || {};
+    currPolicy.children[child] = populatePolicy(
+      childPolicy,
+      currTree,
+      childPolicy.scope,
+      currPolicy.verbs
+    );
+  }
+
+  return currPolicy;
+};

+ 25 - 0
dashboard/src/shared/auth/types.ts

@@ -0,0 +1,25 @@
+export type ScopeType =
+  | "project"
+  | "cluster"
+  | "settings"
+  | "namespace"
+  | "application";
+
+export type Verbs = "get" | "list" | "create" | "update" | "delete";
+
+export interface PolicyDocType {
+  scope: ScopeType;
+  verbs: Array<Verbs>;
+  resources: string[];
+  children?: Partial<Record<ScopeType, PolicyDocType>>;
+}
+
+export enum ScopeTypeEnum {
+  PROJECT = "project",
+  CLUSTER = "cluster",
+  SETTINGS = "settings",
+  NAMESPACE = "namespace",
+  APPLICATION = "application",
+}
+
+export type HIERARCHY_TREE = { [key: string]: any };