jusrhee преди 2 години
родител
ревизия
708a8f05e1

+ 3 - 0
api/types/project.go

@@ -24,6 +24,7 @@ type ProjectList struct {
 	ValidateApplyV2        bool   `json:"validate_apply_v2"`
 	AdvancedInfraEnabled   bool   `json:"advanced_infra_enabled"`
 	SandboxEnabled         bool   `json:"sandbox_enabled"`
+	AdvancedRbacEnabled    bool   `json:"advanced_rbac_enabled"`
 }
 
 // Project type for entries in api responses for everything other than `GET /projects`
@@ -54,6 +55,7 @@ type Project struct {
 	ManagedDeploymentTargetsEnabled bool    `json:"managed_deployment_targets_enabled"`
 	AdvancedInfraEnabled            bool    `json:"advanced_infra_enabled"`
 	SandboxEnabled                  bool    `json:"sandbox_enabled"`
+	AdvancedRbacEnabled             bool    `json:"advanced_rbac_enabled"`
 }
 
 // FeatureFlags is a struct that contains old feature flag representations
@@ -74,6 +76,7 @@ type FeatureFlags struct {
 	StacksEnabled                   string `json:"stacks_enabled,omitempty"`
 	ValidateApplyV2                 bool   `json:"validate_apply_v2"`
 	ManagedDeploymentTargetsEnabled bool   `json:"managed_deployment_targets_enabled"`
+	AdvancedRbacEnabled						  bool   `json:"advanced_rbac_enabled"`
 }
 
 // CreateProjectRequest is a struct that contains the information

+ 9 - 0
dashboard/src/assets/role.svg

@@ -0,0 +1,9 @@
+<svg width="17" height="15" viewBox="0 0 17 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1 14C2.13448 12.084 4.62087 10.7727 8.5 10.7727C12.3791 10.7727 14.8655 12.084 16 14M11.5814 4.12C11.5814 5.84313 10.2018 7.24 8.5 7.24C6.79819 7.24 5.41861 5.84313 5.41861 4.12C5.41861 2.39687 6.79819 1 8.5 1C10.2018 1 11.5814 2.39687 11.5814 4.12Z" stroke="url(#paint0_linear_1586_6)" stroke-width="1.4" stroke-linecap="round"/>
+<defs>
+<linearGradient id="paint0_linear_1586_6" x1="1" y1="1" x2="16" y2="18.5" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F8F8F8"/>
+<stop offset="1" stop-color="#484849"/>
+</linearGradient>
+</defs>
+</svg>

+ 18 - 21
dashboard/src/components/porter/Back.tsx

@@ -1,20 +1,17 @@
-import React, { useEffect, useState } from "react";
+import React from "react";
+import { Link } from "react-router-dom";
 import styled from "styled-components";
 
 import leftArrow from "assets/left-arrow.svg";
-import Text from "./Text";
+
 import Container from "./Container";
-import { Link } from "react-router-dom";
 
 type Props = {
   to?: string;
   onClick?: () => void;
 };
 
-const Back: React.FC<Props> = ({
-  to,
-  onClick,
-}) => {
+const Back: React.FC<Props> = ({ to, onClick }) => {
   return (
     <Container row>
       {to ? (
@@ -57,17 +54,17 @@ const BackLink = styled(Link)`
 `;
 
 const StyledBack = styled.div`
-color: #aaaabb88;
-font-size: 13px;
-margin-bottom: 15px;
-display: flex;
-margin-top: -10px;
-z-index: 999;
-padding: 5px;
-padding-right: 7px;
-border-radius: 5px;
-cursor: pointer;
-:hover {
-  background: #ffffff11;
-}
-`;
+  color: #aaaabb88;
+  font-size: 13px;
+  margin-bottom: 15px;
+  display: flex;
+  margin-top: -10px;
+  z-index: 999;
+  padding: 5px;
+  padding-right: 7px;
+  border-radius: 5px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff11;
+  }
+`;

+ 39 - 29
dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx

@@ -1,40 +1,50 @@
 import React, { useEffect, useRef } from "react";
+
 import Modal from "components/porter/Modal";
-import TitleSection from "components/TitleSection";
 import Text from "components/porter/Text";
+import TitleSection from "components/TitleSection";
+
 import danger from "assets/danger.svg";
 
+import { type PorterLog } from "../logs/types";
 import ExpandedIncidentLogs from "./ExpandedIncidentLogs";
-import { PorterLog } from "../logs/types";
 
-interface LogsModalProps {
-    logs: PorterLog[];
-    setModalVisible: (x: boolean) => void;
-    logsName: string;
-}
-const LogsModal: React.FC<LogsModalProps> = ({ logs, logsName, setModalVisible }) => {
-    const scrollToBottomRef = useRef<HTMLDivElement>(null);
-    const scrollToBottom = () => {
-        if (scrollToBottomRef.current) {
-            scrollToBottomRef.current.scrollIntoView({
-                behavior: "smooth",
-                block: "end",
-            });
-        }
+type LogsModalProps = {
+  logs: PorterLog[];
+  setModalVisible: (x: boolean) => void;
+  logsName: string;
+};
+const LogsModal: React.FC<LogsModalProps> = ({
+  logs,
+  logsName,
+  setModalVisible,
+}) => {
+  const scrollToBottomRef = useRef<HTMLDivElement>(null);
+  const scrollToBottom = () => {
+    if (scrollToBottomRef.current) {
+      scrollToBottomRef.current.scrollIntoView({
+        behavior: "smooth",
+        block: "end",
+      });
     }
-    useEffect(() => {
-        scrollToBottom();
-    }, [scrollToBottomRef]);
-
+  };
+  useEffect(() => {
+    scrollToBottom();
+  }, [scrollToBottomRef]);
 
-    return (
-        <Modal closeModal={() => setModalVisible(false)} width={"800px"}>
-            <TitleSection icon={danger}>
-                <Text size={16}>Logs for {logsName}</Text>
-            </TitleSection>
-            <ExpandedIncidentLogs logs={logs} />
-        </Modal>
-    );
+  return (
+    <Modal
+      closeModal={() => {
+        setModalVisible(false);
+      }}
+      width={"800px"}
+    >
+      <TitleSection icon={danger}>
+        <Text size={16}>Logs for {logsName}</Text>
+      </TitleSection>
+      <ExpandedIncidentLogs logs={logs} />
+    </Modal>
+  );
 };
 
-export default LogsModal;
+export default LogsModal;

+ 101 - 40
dashboard/src/main/home/project-settings/InviteList.tsx

@@ -1,19 +1,24 @@
 import React, { useContext, useEffect, useMemo, useState } from "react";
+import { type Column } from "react-table";
 import styled from "styled-components";
 
-import { type InviteType } from "shared/types";
-import api from "shared/api";
-import { Context } from "shared/Context";
-
-import Loading from "components/Loading";
-import InputRow from "components/form-components/InputRow";
-import Helper from "components/form-components/Helper";
-import Heading from "components/form-components/Heading";
 import CopyToClipboard from "components/CopyToClipboard";
-import { type Column } from "react-table";
+import Heading from "components/form-components/Heading";
+import Helper from "components/form-components/Helper";
+import InputRow from "components/form-components/InputRow";
+import Loading from "components/Loading";
 import Table from "components/OldTable";
+import Button from "components/porter/Button";
+import Spacer from "components/porter/Spacer";
 import RadioSelector from "components/RadioSelector";
 
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { type InviteType } from "shared/types";
+
+import PermissionGroup from "./PermissionGroup";
+import RoleModal from "./RoleModal";
+
 type Props = {};
 
 export type Collaborator = {
@@ -24,7 +29,7 @@ export type Collaborator = {
   kind: string;
 };
 
-const InvitePage: React.FunctionComponent<Props> = ({ }) => {
+const InvitePage: React.FunctionComponent<Props> = ({}) => {
   const {
     currentProject,
     setCurrentModal,
@@ -150,7 +155,9 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
         }
       )
       .then(getData)
-      .catch((err) => { console.log(err); });
+      .catch((err) => {
+        console.log(err);
+      });
   };
 
   const replaceInvite = (
@@ -164,15 +171,16 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
         { email: inviteEmail, kind },
         { id: currentProject.id }
       )
-      .then(async () =>
-        await api.deleteInvite(
-          "<token>",
-          {},
-          {
-            id: currentProject.id,
-            invId: inviteId,
-          }
-        )
+      .then(
+        async () =>
+          await api.deleteInvite(
+            "<token>",
+            {},
+            {
+              id: currentProject.id,
+              invId: inviteId,
+            }
+          )
       )
       .then(getData)
       .catch((err) => {
@@ -188,7 +196,8 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
     const trimmedEmail = email.trim();
     setEmail(trimmedEmail);
 
-    const regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+    const regex =
+      /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
     if (!regex.test(trimmedEmail.toLowerCase())) {
       setIsInvalidEmail(true);
       return;
@@ -222,13 +231,15 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
   };
 
   const columns = useMemo<
-    Array<Column<{
-      email: string;
-      id: number;
-      status: string;
-      invite_link: string;
-      kind: string;
-    }>>
+    Array<
+      Column<{
+        email: string;
+        id: number;
+        status: string;
+        invite_link: string;
+        kind: string;
+      }>
+    >
   >(
     () => [
       {
@@ -258,13 +269,13 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
           if (row.values.status === "expired") {
             return (
               <NewLinkButton
-                onClick={() =>
-                  { replaceInvite(
+                onClick={() => {
+                  replaceInvite(
                     row.values.email,
                     row.original.id,
                     row.values.kind
-                  ); }
-                }
+                  );
+                }}
               >
                 <u>Generate a new link</u>
               </NewLinkButton>
@@ -298,13 +309,17 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
               <Flex>
                 <SettingsButton
                   invis={row.original.currentUser}
-                  onClick={() => { openEditModal(row.original); }}
+                  onClick={() => {
+                    openEditModal(row.original);
+                  }}
                 >
                   <i className="material-icons">more_vert</i>
                 </SettingsButton>
                 <DeleteButton
                   invis={row.original.currentUser}
-                  onClick={() => { removeCollaborator(row.original.id); }}
+                  onClick={() => {
+                    removeCollaborator(row.original.id);
+                  }}
                 >
                   <i className="material-icons">delete</i>
                 </DeleteButton>
@@ -315,13 +330,17 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
             <Flex>
               <SettingsButton
                 invis={row.original.currentUser}
-                onClick={() => { openEditModal(row.original); }}
+                onClick={() => {
+                  openEditModal(row.original);
+                }}
               >
                 <i className="material-icons">more_vert</i>
               </SettingsButton>
               <DeleteButton
                 invis={row.original.currentUser}
-                onClick={() => { deleteInvite(row.original.id); }}
+                onClick={() => {
+                  deleteInvite(row.original.id);
+                }}
               >
                 <i className="material-icons">delete</i>
               </DeleteButton>
@@ -338,7 +357,8 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
     inviteList.sort((a: any, b: any) => (a.email > b.email ? 1 : -1));
     inviteList.sort((a: any, b: any) => (a.accepted > b.accepted ? 1 : -1));
     const buildInviteLink = (token: string) => `
-      ${isHTTPS ? "https://" : ""}${window.location.host}/api/projects/${currentProject.id
+      ${isHTTPS ? "https://" : ""}${window.location.host}/api/projects/${
+        currentProject.id
       }/invites/${token}
     `;
 
@@ -404,18 +424,36 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
   return (
     <>
       <>
+        {currentProject?.advanced_rbac_enabled && (
+          <>
+            <Heading isAtTop={true}>Permission groups</Heading>
+            <Helper>Manage permission groups for your organization.</Helper>
+            <PermissionGroup>Admin</PermissionGroup>
+            <PermissionGroup>Developer</PermissionGroup>
+            <PermissionGroup>Viewer</PermissionGroup>
+            <Spacer y={0.4} />
+            <Button alt>
+              <I className="material-icons">add</I>
+              New group
+            </Button>
+            <Spacer y={1.7} />
+            <RoleModal />
+          </>
+        )}
         <Heading isAtTop={true}>Share project</Heading>
         <Helper>Generate a project invite for another user.</Helper>
         <InputRowWrapper>
           <InputRow
             value={email}
             type="text"
-            setValue={(newEmail: string) => { setEmail(newEmail); }}
+            setValue={(newEmail: string) => {
+              setEmail(newEmail);
+            }}
             width="100%"
             placeholder="ex: mrp@porter.run"
           />
         </InputRowWrapper>
-        <Helper>Specify a role for this user.</Helper>
+        <Helper>Specify a project role for this user.</Helper>
         <RoleSelectorWrapper>
           <RadioSelector
             selected={role}
@@ -424,7 +462,12 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
           />
         </RoleSelectorWrapper>
         <ButtonWrapper>
-          <InviteButton disabled={!hasSeats} onClick={() => { validateEmail(); }}>
+          <InviteButton
+            disabled={!hasSeats}
+            onClick={() => {
+              validateEmail();
+            }}
+          >
             Create invite
           </InviteButton>
           {isInvalidEmail && (
@@ -462,6 +505,24 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
 
 export default InvitePage;
 
+const I = styled.i`
+  margin-right: 10px;
+  font-size: 18px;
+`;
+
+const Group = styled.div`
+  display: inline-block;
+  border-radius: 5px;
+  margin-right: 10px;
+  cursor: pointer;
+  font-size: 13px;
+  padding: 7px 10px;
+  background: ${({ theme }) => theme.clickable.bg};
+  border: 1px solid ${({ theme }) => theme.border};
+  width: fit-content;
+  margin-bottom: 15px;
+`;
+
 const Flex = styled.div`
   display: flex;
   align-items: center;

+ 27 - 0
dashboard/src/main/home/project-settings/PermissionGroup.tsx

@@ -0,0 +1,27 @@
+import React from "react";
+
+import Container from "components/porter/Container";
+import Image from "components/porter/Image";
+import Input from "components/porter/Input";
+import Modal from "components/porter/Modal";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+
+import role from "assets/role.svg";
+
+import RoleModal from "./RoleModal";
+
+type PermissionGroupProps = {
+  foo?: boolean;
+};
+
+const PermissionGroup: React.FC<PermissionGroupProps> = ({ foo }) => {
+  return (
+    <>
+      foo
+      <RoleModal />
+    </>
+  );
+};
+
+export default PermissionGroup;

+ 42 - 39
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -1,28 +1,33 @@
 import React, { Component, useContext, useEffect, useState } from "react";
+import _ from "lodash";
+import {
+  withRouter,
+  WithRouterProps,
+  type RouteComponentProps,
+} from "react-router";
 import styled from "styled-components";
 
+import Heading from "components/form-components/Heading";
+import Helper from "components/form-components/Helper";
+import Button from "components/porter/Button";
+import Error from "components/porter/Error";
+import Input from "components/porter/Input";
+import Link from "components/porter/Link";
+import Spacer from "components/porter/Spacer";
+import TabRegion from "components/TabRegion";
+
+import api from "shared/api";
+import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc";
+import { isAlphanumeric } from "shared/common";
 import { Context } from "shared/Context";
+import { getQueryParam } from "shared/routing";
 import settingsGrad from "assets/settings-grad.svg";
 
-import InvitePage from "./InviteList";
-import TabRegion from "components/TabRegion";
-import Heading from "components/form-components/Heading";
-import Helper from "components/form-components/Helper";
 import DashboardHeader from "../cluster-dashboard/DashboardHeader";
-import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
-import { RouteComponentProps, withRouter, WithRouterProps } from "react-router";
-import { getQueryParam } from "shared/routing";
 import APITokensSection from "./APITokensSection";
-import _ from "lodash";
-import Link from "components/porter/Link";
-import Spacer from "components/porter/Spacer";
-import ProjectDeleteConsent from "./ProjectDeleteConsent";
+import InvitePage from "./InviteList";
 import Metadata from "./Metadata";
-import Button from "components/porter/Button";
-import Input from "components/porter/Input";
-import { isAlphanumeric } from "shared/common";
-import api from "shared/api";
-import Error from "components/porter/Error";
+import ProjectDeleteConsent from "./ProjectDeleteConsent";
 
 type PropsType = RouteComponentProps & WithAuthProps & {};
 type ValidationError = {
@@ -32,7 +37,7 @@ type ValidationError = {
 type StateType = {
   projectName: string;
   currentTab: string;
-  tabOptions: { value: string; label: string }[];
+  tabOptions: Array<{ value: string; label: string }>;
   showCostConfirmModal: boolean;
 };
 
@@ -48,8 +53,7 @@ function ProjectSettings(props: any) {
   const [buttonStatus, setButtonStatus] = useState<React.ReactNode>("");
 
   useEffect(() => {
-    const selectedTab =
-      getQueryParam(props, "selected_tab") || "manage-access";
+    const selectedTab = getQueryParam(props, "selected_tab") || "manage-access";
 
     if (currentTab !== selectedTab) {
       setCurrentTab(selectedTab);
@@ -60,12 +64,10 @@ function ProjectSettings(props: any) {
     if (projectName !== currentProject.name) {
       setProjectName(currentProject.name);
     }
-
   }, []);
 
-
   useEffect(() => {
-    let { currentProject } = context;
+    const { currentProject } = context;
     if (projectName !== currentProject.name) {
       setProjectName(currentProject.name);
     }
@@ -99,7 +101,6 @@ function ProjectSettings(props: any) {
       });
     }
 
-
     if (!_.isEqual(tabOpts, tabOptions)) {
       setTabOptions(tabOpts);
     }
@@ -108,7 +109,6 @@ function ProjectSettings(props: any) {
     if (selectedTab && selectedTab !== currentTab) {
       setCurrentTab(selectedTab);
     }
-
   }, [context, projectName, currentTab, props, tabOptions]);
 
   const validateProjectName = (): ValidationError => {
@@ -145,19 +145,19 @@ function ProjectSettings(props: any) {
       await api.renameProject(
         "<token>",
         {
-          name: name,
+          name,
         },
         {
           project_id: context.currentProject.id,
-        })
+        }
+      );
       setButtonStatus("success");
       window.location.reload();
-
     } catch (err) {
-      console.log(err)
+      console.log(err);
       setButtonStatus(<Error message="Unable to rename project" />);
     }
-  }
+  };
 
   const renderTabContents = () => {
     if (!props.isAuthorized("settings", "", ["get", "delete"])) {
@@ -166,9 +166,8 @@ function ProjectSettings(props: any) {
 
     if (currentTab === "manage-access") {
       return <InvitePage />;
-    }
-    else if (currentTab == "metadata") {
-      return <Metadata />
+    } else if (currentTab == "metadata") {
+      return <Metadata />;
     } else if (currentTab === "api-tokens") {
       return <APITokensSection />;
     } else if (currentTab === "billing") {
@@ -188,18 +187,23 @@ function ProjectSettings(props: any) {
     } else {
       return (
         <>
-
           <Heading isAtTop={true}>Rename Project</Heading>
 
-          <Helper color={validateProjectName().hasError ? "#f5cb42" : "#aaaabb"}>
+          <Helper
+            color={validateProjectName().hasError ? "#f5cb42" : "#aaaabb"}
+          >
             (lowercase letters, numbers, and "-" only)
           </Helper>
-          <Input placeholder={"ex: perspective-vortex"} value={name} setValue={setName} width={"500px"}>
-          </Input>
+          <Input
+            placeholder={"ex: perspective-vortex"}
+            value={name}
+            setValue={setName}
+            width={"500px"}
+          ></Input>
           <Spacer y={1} />
           <Button
             onClick={() => {
-              handleNameChange()
+              handleNameChange();
             }}
             status={buttonStatus}
             loadingText={"Updating..."}
@@ -228,7 +232,7 @@ function ProjectSettings(props: any) {
           </DeleteButton>
           <ProjectDeleteConsent
             setShowCostConfirmModal={setShowCostConfirmModal}
-            show={showCostConfirmModal}  // <-- Pass these props
+            show={showCostConfirmModal} // <-- Pass these props
           />
         </>
       );
@@ -254,7 +258,6 @@ function ProjectSettings(props: any) {
   );
 }
 
-
 ProjectSettings.contextType = Context;
 
 export default withRouter(withAuth(ProjectSettings));

+ 36 - 0
dashboard/src/main/home/project-settings/RoleModal.tsx

@@ -0,0 +1,36 @@
+import React from "react";
+
+import Container from "components/porter/Container";
+import Image from "components/porter/Image";
+import Input from "components/porter/Input";
+import Modal from "components/porter/Modal";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+
+import role from "assets/role.svg";
+
+type RoleModalProps = {
+  foo?: boolean;
+};
+
+const RoleModal: React.FC<RoleModalProps> = ({ foo }) => {
+  return (
+    <Modal closeModal={() => {}} width={"800px"}>
+      <Container row>
+        <Image src={role} />
+        <Spacer inline x={1} />
+        <Text size={16}>Configure role</Text>
+      </Container>
+      <Spacer y={1} />
+      <Input
+        placeholder="ex: admin"
+        width="300px"
+        value="Admin"
+        setValue={() => {}}
+      />
+      <Spacer y={1} />
+    </Modal>
+  );
+};
+
+export default RoleModal;

+ 1 - 0
dashboard/src/shared/types.tsx

@@ -334,6 +334,7 @@ export type ProjectType = {
   managed_deployment_targets_enabled: boolean;
   aws_ack_auth_enabled: boolean;
   sandbox_enabled: boolean;
+  advanced_rbac_enabled: boolean;
   roles: Array<{
     id: number;
     kind: string;

+ 0 - 2
go.sum

@@ -1523,8 +1523,6 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.2.113 h1:sv1huO9MpykJaWhV2D5zTD2LouMbRSBV5ATt/5Ukrbo=
-github.com/porter-dev/api-contracts v0.2.113/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/api-contracts v0.2.114 h1:qfEq70BJ8xXTkiZU7ygzOSGnMCqJHOa5Lbkfu4OzQBI=
 github.com/porter-dev/api-contracts v0.2.114/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=

+ 9 - 0
internal/models/project.go

@@ -81,6 +81,9 @@ const (
 
 	// AdvancedInfraEnabled controls whether a project can use advanced infrastructure settings
 	AdvancedInfraEnabled FeatureFlagLabel = "advanced_infra_enabled"
+
+	// AdvancedRbacEnabled controls whether a project can use advanced rbac settings
+	AdvancedRbacEnabled FeatureFlagLabel = "advanced_rbac_enabled"
 )
 
 // ProjectFeatureFlags keeps track of all project-related feature flags
@@ -107,6 +110,7 @@ var ProjectFeatureFlags = map[FeatureFlagLabel]bool{
 	ValidateApplyV2:                 true,
 	ManagedDeploymentTargetsEnabled: false,
 	AdvancedInfraEnabled:            false,
+	AdvancedRbacEnabled:             false,
 }
 
 type ProjectPlan string
@@ -201,6 +205,7 @@ type Project struct {
 	EnableSandbox        bool `gorm:"default:false"`
 	EnableReprovision    bool `gorm:"default:false"`
 	AdvancedInfraEnabled bool `gorm:"default:false"`
+	AdvancedRbacEnabled	 bool `gorm:"default:false"`
 }
 
 // GetFeatureFlag calls launchdarkly for the specified flag
@@ -249,6 +254,8 @@ func (p *Project) GetFeatureFlag(flagName FeatureFlagLabel, launchDarklyClient *
 			return false
 		case "advanced_infra_enabled":
 			return false
+		case "advanced_rbac_enabled":
+			return p.AdvancedRbacEnabled
 		}
 	}
 
@@ -299,6 +306,7 @@ func (p *Project) ToProjectType(launchDarklyClient *features.Client) types.Proje
 		ManagedDeploymentTargetsEnabled: p.GetFeatureFlag(ManagedDeploymentTargetsEnabled, launchDarklyClient),
 		AdvancedInfraEnabled:            p.GetFeatureFlag(AdvancedInfraEnabled, launchDarklyClient),
 		SandboxEnabled:                  p.EnableSandbox,
+		AdvancedRbacEnabled:             p.GetFeatureFlag(AdvancedRbacEnabled, launchDarklyClient),
 	}
 }
 
@@ -334,6 +342,7 @@ func (p *Project) ToProjectListType() *types.ProjectList {
 		ValidateApplyV2:        p.ValidateApplyV2,
 		FullAddOns:             p.FullAddOns,
 		AdvancedInfraEnabled:   p.AdvancedInfraEnabled,
+		AdvancedRbacEnabled:    p.AdvancedRbacEnabled, 
 	}
 }