From 62cb026ac7452d52988c6b2a9b4a04e1cc102c95 Mon Sep 17 00:00:00 2001 From: Alejandra Buznego Date: Fri, 8 Jul 2022 16:30:26 -0700 Subject: [PATCH 1/3] use vetted attribute to limit add organization action on backend --- lib/console/auth/auth.ex | 9 +-- lib/console/auth/user.ex | 1 + .../controllers/organization_controller.ex | 62 ++++++++++--------- .../20220708223548_user_vetted_attribute.exs | 9 +++ 4 files changed, 48 insertions(+), 33 deletions(-) create mode 100644 priv/repo/migrations/20220708223548_user_vetted_attribute.exs diff --git a/lib/console/auth/auth.ex b/lib/console/auth/auth.ex index b0db18355..c20b696b1 100644 --- a/lib/console/auth/auth.ex +++ b/lib/console/auth/auth.ex @@ -16,13 +16,14 @@ defmodule Console.Auth do end def get_user_by_id_and_email(user_id, email) do + vetted = get_user_by_email(email).vetted case get_user_by_id(user_id) do - %{super: is_super} -> get_user_data_map(user_id, email, is_super) - _ -> get_user_data_map(user_id, email) + %{super: is_super} -> get_user_data_map(user_id, email, vetted, is_super) + _ -> get_user_data_map(user_id, email, vetted) end end - defp get_user_data_map(user_id, user_email, super_user \\ false) do - %User{id: user_id, super: super_user, email: user_email} + defp get_user_data_map(user_id, user_email, vetted, super_user \\ false) do + %User{id: user_id, super: super_user, email: user_email, vetted: vetted} end end diff --git a/lib/console/auth/user.ex b/lib/console/auth/user.ex index 27843bb3c..857745f8f 100644 --- a/lib/console/auth/user.ex +++ b/lib/console/auth/user.ex @@ -13,6 +13,7 @@ defmodule Console.Auth.User do field :confirmed_at, :naive_datetime field :last_2fa_skipped_at, :naive_datetime field :super, :boolean + field :vetted, :boolean has_many :memberships, Console.Organizations.Membership has_many :api_keys, Console.ApiKeys.ApiKey diff --git a/lib/console_web/controllers/organization_controller.ex b/lib/console_web/controllers/organization_controller.ex index 882cd62fc..55bc3cd02 100644 --- a/lib/console_web/controllers/organization_controller.ex +++ b/lib/console_web/controllers/organization_controller.ex @@ -36,38 +36,42 @@ defmodule ConsoleWeb.OrganizationController do end def create(conn, %{"organization" => %{ "name" => organization_name, "from" => _ } }) do - with {:ok, %Organization{} = organization} <- - Organizations.create_organization(conn.assigns.current_user, %{ "name" => organization_name }) do - organizations = Organizations.get_organizations(conn.assigns.current_user) - membership = Organizations.get_membership!(conn.assigns.current_user, organization) - membership_info = %{id: organization.id, name: organization.name, role: membership.role} - - Task.Supervisor.async_nolink(ConsoleWeb.TaskSupervisor, fn -> - OrgIps.create_org_ip(%{ - "address" => ConsoleWeb.IPFilter.get_ip(conn), - "email" => membership.email, - "organization_id" => organization.id, - "organization_name" => organization.name, - "banned" => false - }) - end) + if Application.get_env(:console, :user_invite_only) == true and not conn.assigns.current_user.vetted do + {:error, :forbidden, "Please contact #{if Application.get_env(:console, :self_hosted) == true do "the admin" else "our sales team" end} to add organizations."} + else + with {:ok, %Organization{} = organization} <- + Organizations.create_organization(conn.assigns.current_user, %{ "name" => organization_name }) do + organizations = Organizations.get_organizations(conn.assigns.current_user) + membership = Organizations.get_membership!(conn.assigns.current_user, organization) + membership_info = %{id: organization.id, name: organization.name, role: membership.role} + + Task.Supervisor.async_nolink(ConsoleWeb.TaskSupervisor, fn -> + OrgIps.create_org_ip(%{ + "address" => ConsoleWeb.IPFilter.get_ip(conn), + "email" => membership.email, + "organization_id" => organization.id, + "organization_name" => organization.name, + "banned" => false + }) + end) - case Enum.count(organizations) do - 1 -> - initial_dc = String.to_integer(System.get_env("INITIAL_ORG_GIFTED_DC") || "10000") - if initial_dc > 0 do - Organizations.update_organization(organization, %{ "dc_balance" => initial_dc, "dc_balance_nonce" => 1, "received_free_dc" => true }) - end + case Enum.count(organizations) do + 1 -> + initial_dc = String.to_integer(System.get_env("INITIAL_ORG_GIFTED_DC") || "10000") + if initial_dc > 0 do + Organizations.update_organization(organization, %{ "dc_balance" => initial_dc, "dc_balance_nonce" => 1, "received_free_dc" => true }) + end - render(conn, "show.json", organization: membership_info) - _ -> - ConsoleWeb.Endpoint.broadcast("graphql:topbar_orgs", "graphql:topbar_orgs:#{conn.assigns.current_user.id}:organization_list_update", %{}) - ConsoleWeb.Endpoint.broadcast("graphql:orgs_index_table", "graphql:orgs_index_table:#{conn.assigns.current_user.id}:organization_list_update", %{}) + render(conn, "show.json", organization: membership_info) + _ -> + ConsoleWeb.Endpoint.broadcast("graphql:topbar_orgs", "graphql:topbar_orgs:#{conn.assigns.current_user.id}:organization_list_update", %{}) + ConsoleWeb.Endpoint.broadcast("graphql:orgs_index_table", "graphql:orgs_index_table:#{conn.assigns.current_user.id}:organization_list_update", %{}) - conn - |> put_status(:created) - |> put_resp_header("message", "Organization #{organization.name} added successfully") - |> render("show.json", organization: membership_info) + conn + |> put_status(:created) + |> put_resp_header("message", "Organization #{organization.name} added successfully") + |> render("show.json", organization: membership_info) + end end end end diff --git a/priv/repo/migrations/20220708223548_user_vetted_attribute.exs b/priv/repo/migrations/20220708223548_user_vetted_attribute.exs new file mode 100644 index 000000000..a15b31b18 --- /dev/null +++ b/priv/repo/migrations/20220708223548_user_vetted_attribute.exs @@ -0,0 +1,9 @@ +defmodule Console.Repo.Migrations.UserVettedAttribute do + use Ecto.Migration + + def change do + alter table(:users) do + add :vetted, :boolean, default: false, null: false + end + end +end From d3720256e18c22fb37466919e752c800d2bdf4b0 Mon Sep 17 00:00:00 2001 From: Alejandra Buznego Date: Mon, 11 Jul 2022 13:39:05 -0700 Subject: [PATCH 2/3] hide button if unvetted user in invite only env --- .../organizations/OrganizationIndex.jsx | 198 +++++++++--------- assets/js/graphql/users.js | 9 + lib/console/auth/auth.ex | 3 +- lib/console/auth/user_resolver.ex | 12 ++ lib/console_web/schema/schema.ex | 9 + 5 files changed, 130 insertions(+), 101 deletions(-) create mode 100644 assets/js/graphql/users.js create mode 100644 lib/console/auth/user_resolver.ex diff --git a/assets/js/components/organizations/OrganizationIndex.jsx b/assets/js/components/organizations/OrganizationIndex.jsx index c47576edc..a3bfa789f 100644 --- a/assets/js/components/organizations/OrganizationIndex.jsx +++ b/assets/js/components/organizations/OrganizationIndex.jsx @@ -1,4 +1,4 @@ -import React, { Component } from "react"; +import React, { useState, useEffect } from "react"; import DashboardLayout from "../common/DashboardLayout"; import { MobileDisplay, DesktopDisplay } from "../mobile/MediaQuery"; import MobileLayout from "../mobile/MobileLayout"; @@ -14,103 +14,104 @@ import { Button, Typography } from "antd"; import PlusOutlined from "@ant-design/icons/PlusOutlined"; const { Text } = Typography; import { isMobile } from "../../util/constants"; +import { useQuery } from "@apollo/client"; +import { GET_VETTED_USER_STATUS } from "../../graphql/users"; -class OrganizationIndex extends Component { - state = { - showOrganizationModal: false, - showDeleteOrganizationModal: false, - selectedOrg: null, - showEditOrganizationModal: false, - }; +export default ({ user }) => { + const { data } = useQuery(GET_VETTED_USER_STATUS, { + variables: { email: user.email }, + skip: !( + window.user_invite_only === "true" || + process.env.USER_INVITE_ONLY === "true" + ), + }); + + const [showOrganizationModal, setShowOrganizationModal] = useState(false); + const [showDeleteOrganizationModal, setShowDeleteOrganizationModal] = + useState(false); + const [selectedOrg, setSelectedOrg] = useState(null); + const [showEditOrganizationModal, setShowEditOrganizationModal] = + useState(false); - componentDidMount() { + useEffect(() => { analyticsLogger.logEvent( isMobile ? "ACTION_NAV_DASHBOARD_MOBILE" : "ACTION_NAV_DASHBOARD" ); - } + }, []); - openOrganizationModal = () => { - this.setState({ showOrganizationModal: true }); + const openOrganizationModal = () => { + setShowOrganizationModal(true); }; - closeOrganizationModal = () => { - this.setState({ showOrganizationModal: false }); + const closeOrganizationModal = () => { + setShowOrganizationModal(false); }; - openDeleteOrganizationModal = (selectedOrg) => { - this.setState({ showDeleteOrganizationModal: true, selectedOrg }); + const openDeleteOrganizationModal = (selectedOrg) => { + setShowDeleteOrganizationModal(true); + setSelectedOrg(selectedOrg); }; - closeDeleteOrganizationModal = () => { - this.setState({ - showDeleteOrganizationModal: false, - selectedOrg: null, - }); + const closeDeleteOrganizationModal = () => { + setShowDeleteOrganizationModal(false); + setSelectedOrg(null); }; - openEditOrganizationModal = (selectedOrg) => { - this.setState({ showEditOrganizationModal: true, selectedOrg }); + const openEditOrganizationModal = (selectedOrg) => { + setShowEditOrganizationModal(true); + setSelectedOrg(selectedOrg); }; - closeEditOrganizationModal = () => { - this.setState({ - showEditOrganizationModal: false, - selectedOrg: null, - }); + const closeEditOrganizationModal = () => { + setShowEditOrganizationModal(false); + setSelectedOrg(null); }; - render() { - const { - showOrganizationModal, - showDeleteOrganizationModal, - selectedOrg, - showEditOrganizationModal, - } = this.state; - return ( - <> - - - - - + return ( + <> + + + + + - - + +
-
-
-
- - All Organizations - - {process.env.IMPOSE_HARD_CAP !== 'true' && ( +
+
+ + All Organizations + + {process.env.IMPOSE_HARD_CAP !== "true" && + (window.user_invite_only === "true" || + process.env.USER_INVITE_ONLY === "true" + ? data?.vettedUserStatus.vetted === true + : true) && ( )} -
-
+
+
- - - - - - - - ); - } -} + -export default OrganizationIndex; + + + + + + ); +}; diff --git a/assets/js/graphql/users.js b/assets/js/graphql/users.js new file mode 100644 index 000000000..69235090f --- /dev/null +++ b/assets/js/graphql/users.js @@ -0,0 +1,9 @@ +import { gql } from "@apollo/client"; + +export const GET_VETTED_USER_STATUS = gql` + query VettedUserStatusQuery($email: String!) { + vettedUserStatus(email: $email) { + vetted + } + } +`; diff --git a/lib/console/auth/auth.ex b/lib/console/auth/auth.ex index c20b696b1..431fd1d6c 100644 --- a/lib/console/auth/auth.ex +++ b/lib/console/auth/auth.ex @@ -16,7 +16,8 @@ defmodule Console.Auth do end def get_user_by_id_and_email(user_id, email) do - vetted = get_user_by_email(email).vetted + user = get_user_by_email(email) + vetted = if is_nil(user) do nil else user.vetted end case get_user_by_id(user_id) do %{super: is_super} -> get_user_data_map(user_id, email, vetted, is_super) _ -> get_user_data_map(user_id, email, vetted) diff --git a/lib/console/auth/user_resolver.ex b/lib/console/auth/user_resolver.ex new file mode 100644 index 000000000..c15ce0d4a --- /dev/null +++ b/lib/console/auth/user_resolver.ex @@ -0,0 +1,12 @@ +defmodule Console.Users.UserResolver do + alias Console.Auth + + def get_vetted_user_status(%{email: email}, %{context: %{current_organization: _}}) do + user = Auth.get_user_by_email(email) + if not is_nil(user) and user.vetted do + {:ok, %{vetted: true}} + else + {:ok, %{vetted: false}} + end + end +end \ No newline at end of file diff --git a/lib/console_web/schema/schema.ex b/lib/console_web/schema/schema.ex index 7faa34b21..a8e37fde8 100644 --- a/lib/console_web/schema/schema.ex +++ b/lib/console_web/schema/schema.ex @@ -152,6 +152,10 @@ defmodule ConsoleWeb.Schema do field :labels, list_of(:label) end + object :vetted_user_status do + field :vetted, :boolean + end + object :group do field :id, :id field :name, :string @@ -607,5 +611,10 @@ defmodule ConsoleWeb.Schema do paginated field :dc_purchases, :paginated_dc_purchases do resolve(&Console.DcPurchases.DcPurchaseResolver.paginate/2) end + + field :vetted_user_status, :vetted_user_status do + arg :email, :string + resolve &Console.Users.UserResolver.get_vetted_user_status/2 + end end end From 6be37ab0bac29c95993499808ef14c9c3b6a8838 Mon Sep 17 00:00:00 2001 From: Alejandra Buznego Date: Mon, 11 Jul 2022 20:47:11 -0700 Subject: [PATCH 3/3] default to true vetted for superuser --- lib/console/auth/auth.ex | 5 ++++- lib/console/auth/user_resolver.ex | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/console/auth/auth.ex b/lib/console/auth/auth.ex index 431fd1d6c..ff6d355d9 100644 --- a/lib/console/auth/auth.ex +++ b/lib/console/auth/auth.ex @@ -18,8 +18,11 @@ defmodule Console.Auth do def get_user_by_id_and_email(user_id, email) do user = get_user_by_email(email) vetted = if is_nil(user) do nil else user.vetted end + case get_user_by_id(user_id) do - %{super: is_super} -> get_user_data_map(user_id, email, vetted, is_super) + %{super: is_super} -> + vetted = if is_super == true do true else vetted end + get_user_data_map(user_id, email, vetted, is_super) _ -> get_user_data_map(user_id, email, vetted) end end diff --git a/lib/console/auth/user_resolver.ex b/lib/console/auth/user_resolver.ex index c15ce0d4a..3c7e03ae0 100644 --- a/lib/console/auth/user_resolver.ex +++ b/lib/console/auth/user_resolver.ex @@ -3,7 +3,7 @@ defmodule Console.Users.UserResolver do def get_vetted_user_status(%{email: email}, %{context: %{current_organization: _}}) do user = Auth.get_user_by_email(email) - if not is_nil(user) and user.vetted do + if not is_nil(user) and (user.vetted == true or user.super == true) do {:ok, %{vetted: true}} else {:ok, %{vetted: false}}