Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit ability to add organization in Nova console (and invite-only envs) #1223

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 98 additions & 100 deletions assets/js/components/organizations/OrganizationIndex.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -14,139 +14,137 @@ 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 (
<>
<MobileDisplay>
<MobileLayout>
<MobileOrganizationIndex user={this.props.user} />
</MobileLayout>
</MobileDisplay>
return (
<>
<MobileDisplay>
<MobileLayout>
<MobileOrganizationIndex user={user} />
</MobileLayout>
</MobileDisplay>

<DesktopDisplay>
<DashboardLayout
title="Organizations"
user={this.props.user}
noAddButton
<DesktopDisplay>
<DashboardLayout title="Organizations" user={user} noAddButton>
<div
style={{
height: "100%",
width: "100%",
backgroundColor: "#ffffff",
borderRadius: 6,
overflow: "hidden",
boxShadow: "0px 20px 20px -7px rgba(17, 24, 31, 0.19)",
}}
>
<div
style={{
height: "100%",
width: "100%",
backgroundColor: "#ffffff",
borderRadius: 6,
overflow: "hidden",
boxShadow: "0px 20px 20px -7px rgba(17, 24, 31, 0.19)",
}}
>
<div style={{ overflowX: "scroll" }} className="no-scroll-bar">
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
padding: "30px 20px 20px 30px",
minWidth,
}}
>
<Text style={{ fontSize: 22, fontWeight: 600 }}>
All Organizations
</Text>
{process.env.IMPOSE_HARD_CAP !== 'true' && (
<div style={{ overflowX: "scroll" }} className="no-scroll-bar">
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
padding: "30px 20px 20px 30px",
minWidth,
}}
>
<Text style={{ fontSize: 22, fontWeight: 600 }}>
All Organizations
</Text>
{process.env.IMPOSE_HARD_CAP !== "true" &&
(window.user_invite_only === "true" ||
process.env.USER_INVITE_ONLY === "true"
? data?.vettedUserStatus.vetted === true
: true) && (
<UserCan noManager>
<Button
icon={<PlusOutlined />}
style={{ borderRadius: 4 }}
onClick={() => {
analyticsLogger.logEvent("ACTION_NEW_ORG");
this.openOrganizationModal();
openOrganizationModal();
}}
type="primary"
>
Add Organization
</Button>
</UserCan>
)}
</div>
<OrganizationsTable
openDeleteOrganizationModal={this.openDeleteOrganizationModal}
openEditOrganizationModal={this.openEditOrganizationModal}
user={this.props.user}
/>
</div>
<OrganizationsTable
openDeleteOrganizationModal={openDeleteOrganizationModal}
openEditOrganizationModal={openEditOrganizationModal}
user={user}
/>
</div>
</div>

<NewOrganizationModal
open={showOrganizationModal}
onClose={this.closeOrganizationModal}
/>

<DeleteOrganizationModal
open={showDeleteOrganizationModal}
onClose={this.closeDeleteOrganizationModal}
selectedOrgId={selectedOrg && selectedOrg.id}
/>
<EditOrganizationModal
open={showEditOrganizationModal}
onClose={this.closeEditOrganizationModal}
selectedOrg={selectedOrg}
/>
</DashboardLayout>
</DesktopDisplay>
</>
);
}
}
<NewOrganizationModal
open={showOrganizationModal}
onClose={closeOrganizationModal}
/>

export default OrganizationIndex;
<DeleteOrganizationModal
open={showDeleteOrganizationModal}
onClose={closeDeleteOrganizationModal}
selectedOrgId={selectedOrg && selectedOrg.id}
/>
<EditOrganizationModal
open={showEditOrganizationModal}
onClose={closeEditOrganizationModal}
selectedOrg={selectedOrg}
/>
</DashboardLayout>
</DesktopDisplay>
</>
);
};
9 changes: 9 additions & 0 deletions assets/js/graphql/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { gql } from "@apollo/client";

export const GET_VETTED_USER_STATUS = gql`
query VettedUserStatusQuery($email: String!) {
vettedUserStatus(email: $email) {
vetted
}
}
`;
10 changes: 6 additions & 4 deletions lib/console/auth/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ defmodule Console.Auth do
end

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, is_super)
_ -> get_user_data_map(user_id, email)
%{super: is_super} -> get_user_data_map(user_id, email, vetted, is_super)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we default superuser to vetted

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added!

_ -> 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
1 change: 1 addition & 0 deletions lib/console/auth/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions lib/console/auth/user_resolver.ex
Original file line number Diff line number Diff line change
@@ -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
62 changes: 33 additions & 29 deletions lib/console_web/controllers/organization_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions lib/console_web/schema/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
9 changes: 9 additions & 0 deletions priv/repo/migrations/20220708223548_user_vetted_attribute.exs
Original file line number Diff line number Diff line change
@@ -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