diff --git a/apps/api/lib/api_web/endpoint.ex b/apps/api/lib/api_web/endpoint.ex index 87bb8b280..a9222c848 100644 --- a/apps/api/lib/api_web/endpoint.ex +++ b/apps/api/lib/api_web/endpoint.ex @@ -33,6 +33,11 @@ defmodule ApiWeb.Endpoint do plug Plug.RequestId plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] + plug Hammer.Plug, [ + rate_limit: {"api.global", 60_000, 1000}, + by: :ip + ] + plug Plug.Parsers, parsers: [:urlencoded, {:multipart, length: @upload_maximum}, ApiWeb.Parsers.ApplicationJson], pass: ["*/*"], diff --git a/apps/api/mix.exs b/apps/api/mix.exs index f03d014e0..ea52b03f0 100644 --- a/apps/api/mix.exs +++ b/apps/api/mix.exs @@ -80,6 +80,7 @@ defmodule Api.MixProject do {:prometheus_ex, "~> 3.0"}, {:prometheus_plugs, "~> 1.1.1"}, {:remote_ip, "~> 0.2.0"}, + {:hammer_plug, "~> 3.0"}, {:k8s_traffic_plug, github: "Financial-Times/k8s_traffic_plug"}, {:core, in_umbrella: true}, diff --git a/apps/core/lib/core/schema/console_instance.ex b/apps/core/lib/core/schema/console_instance.ex index 4f532dbbb..dcfeb678f 100644 --- a/apps/core/lib/core/schema/console_instance.ex +++ b/apps/core/lib/core/schema/console_instance.ex @@ -16,7 +16,12 @@ defmodule Core.Schema.ConsoleInstance do stack_deleted: 7 @region_map %{ - aws: ~w(us-east-1) + shared: %{ + aws: ~w(us-east-1) + }, + dedicated: %{ + aws: ~w(us-east-1 me-central-1) + } } schema "console_instances" do @@ -134,7 +139,8 @@ defmodule Core.Schema.ConsoleInstance do defp validate_region(cs) do cloud = get_field(cs, :cloud) - regions = @region_map[cloud] + type = get_field(cs, :type) + regions = @region_map[type][cloud] validate_change(cs, :region, fn :region, reg -> case reg in regions do true -> [] diff --git a/apps/core/lib/core/schema/user.ex b/apps/core/lib/core/schema/user.ex index f34a6ab01..2db594675 100644 --- a/apps/core/lib/core/schema/user.ex +++ b/apps/core/lib/core/schema/user.ex @@ -204,7 +204,7 @@ defmodule Core.Schema.User do |> validate_required([:name, :email]) |> validate_length(:email, max: 255) |> validate_length(:name, max: 255) - |> validate_length(:password, min: 10) + |> validate_length(:password, min: 10, max: 255) |> validate_format(:email, @email_re) |> hash_password() |> generate_uuid(:avatar_id) @@ -254,7 +254,7 @@ defmodule Core.Schema.User do |> add_email(model) |> validate_length(:email, max: 255) |> validate_length(:name, max: 255) - |> validate_length(:password, min: 10) + |> validate_length(:password, min: 10, max: 255) |> validate_format(:email, @email_re) |> unique_constraint(:email) |> validate_required([:name, :email]) diff --git a/apps/core/lib/core/services/cloud/workflow.ex b/apps/core/lib/core/services/cloud/workflow.ex index 311871bbf..b4ba31e3a 100644 --- a/apps/core/lib/core/services/cloud/workflow.ex +++ b/apps/core/lib/core/services/cloud/workflow.ex @@ -1,7 +1,6 @@ defmodule Core.Services.Cloud.Workflow do use Core.Services.Base alias Core.Repo - alias Core.Clients.Console alias Core.Schema.{ConsoleInstance} alias Core.Services.Cloud.Workflow.{Dedicated, Shared} diff --git a/apps/core/lib/core/services/cloud/workflow/shared.ex b/apps/core/lib/core/services/cloud/workflow/shared.ex index 1be796b43..2732f4416 100644 --- a/apps/core/lib/core/services/cloud/workflow/shared.ex +++ b/apps/core/lib/core/services/cloud/workflow/shared.ex @@ -8,6 +8,8 @@ defmodule Core.Services.Cloud.Workflow.Shared do alias Core.Schema.{ConsoleInstance, PostgresCluster, User} alias Core.Repo + require Logger + @behaviour Core.Services.Cloud.Workflow def sync(%ConsoleInstance{external_id: id} = instance) when is_binary(id) do diff --git a/apps/core/lib/core/services/users.ex b/apps/core/lib/core/services/users.ex index b0d765adc..6b00a5bfd 100644 --- a/apps/core/lib/core/services/users.ex +++ b/apps/core/lib/core/services/users.ex @@ -715,8 +715,8 @@ defmodule Core.Services.Users do |> when_ok(&delete_eab_key/1) end - @decorate cache_evict(cache: Core.Cache, keys: [{:eab, c, p, u}]) - def delete_eab_key(%EabCredential{user_id: u, cluster: c, provider: p} = eab), + @decorate cache_evict(cache: Core.Cache, keys: [{:eab, eab.user_id, eab.cluster, eab.provider}]) + def delete_eab_key(%EabCredential{} = eab), do: Core.Repo.delete(eab) defp materialize_eab_key(cluster, provider, %User{id: user_id}) do diff --git a/apps/graphql/lib/graphql/resolvers/cloud.ex b/apps/graphql/lib/graphql/resolvers/cloud.ex index b7703f4ee..0925fe2c9 100644 --- a/apps/graphql/lib/graphql/resolvers/cloud.ex +++ b/apps/graphql/lib/graphql/resolvers/cloud.ex @@ -2,6 +2,12 @@ defmodule GraphQl.Resolvers.Cloud do use GraphQl.Resolvers.Base, model: Core.Schema.ConsoleInstance alias Core.Services.{Cloud, Clusters} + def resolve_settings(_, _) do + {:ok, %{ + regions: ConsoleInstance.regions() + }} + end + def resolve_instance(%{id: id}, %{context: %{current_user: user}}), do: Cloud.visible(id, user) diff --git a/apps/graphql/lib/graphql/schema/cloud.ex b/apps/graphql/lib/graphql/schema/cloud.ex index 9ae2661c1..6fb77849d 100644 --- a/apps/graphql/lib/graphql/schema/cloud.ex +++ b/apps/graphql/lib/graphql/schema/cloud.ex @@ -45,9 +45,28 @@ defmodule GraphQl.Schema.Cloud do timestamps() end + object :plural_cloud_settings do + field :regions, :plural_cloud_regions + end + + object :plural_cloud_regions do + field :shared, non_null(:cloud_regions) + field :dedicated, non_null(:cloud_regions) + end + + object :cloud_regions do + field :aws, list_of(:string) + end + connection node_type: :console_instance object :cloud_queries do + field :cloud_settings, :plural_cloud_settings do + middleware Authenticated + + resolve &Cloud.resolve_settings/2 + end + field :console_instance, :console_instance do middleware Authenticated arg :id, non_null(:id) diff --git a/apps/graphql/lib/graphql/schema/user.ex b/apps/graphql/lib/graphql/schema/user.ex index 334ec62ac..bf1bcd928 100644 --- a/apps/graphql/lib/graphql/schema/user.ex +++ b/apps/graphql/lib/graphql/schema/user.ex @@ -476,7 +476,7 @@ defmodule GraphQl.Schema.User do arg :email, non_null(:string) arg :password, non_null(:string) arg :device_token, :string - arg :captcha, :string + arg :captcha, non_null(:string) middleware Captcha @@ -558,6 +558,7 @@ defmodule GraphQl.Schema.User do field :update_user, :user do middleware Authenticated + middleware RateLimit, limit: 10, time: 60_000 arg :id, :id arg :attributes, non_null(:user_attributes) diff --git a/apps/graphql/test/mutations/user_mutation_test.exs b/apps/graphql/test/mutations/user_mutation_test.exs index 66f0dcb43..7bf547155 100644 --- a/apps/graphql/test/mutations/user_mutation_test.exs +++ b/apps/graphql/test/mutations/user_mutation_test.exs @@ -13,13 +13,17 @@ defmodule GraphQl.UserMutationTest do }) {:ok, %{data: %{"login" => found}}} = run_query(""" - mutation Login($email: String!, $password: String!) { - login(email: $email, password: $password) { + mutation Login($email: String!, $password: String!, $captcha: String!) { + login(email: $email, password: $password, captcha: $captcha) { id jwt } } - """, %{"email" => "mjg@plural.sh", "password" => "super strong password"}, %{origin: "https://app.plural.sh"}) + """, %{ + "email" => "mjg@plural.sh", + "password" => "super strong password", + "captcha" => "valid_response" + }, %{origin: "https://app.plural.sh"}) assert found["id"] == user.id assert found["jwt"] @@ -112,8 +116,8 @@ defmodule GraphQl.UserMutationTest do token = insert(:login_token) {:ok, %{data: %{"login" => found}}} = run_query(""" - mutation Login($email: String!, $password: String!, $deviceToken: String) { - login(email: $email, password: $password, deviceToken: $deviceToken) { + mutation Login($email: String!, $password: String!, $deviceToken: String, $captcha: String!) { + login(email: $email, password: $password, deviceToken: $deviceToken, captcha: $captcha) { id jwt } @@ -121,7 +125,8 @@ defmodule GraphQl.UserMutationTest do """, %{ "email" => "mjg@plural.sh", "password" => "super strong password", - "deviceToken" => token.token + "deviceToken" => token.token, + "captcha" => "valid_response" }) assert found["id"] == user.id diff --git a/apps/graphql/test/queries/cloud_queries_test.exs b/apps/graphql/test/queries/cloud_queries_test.exs index 0407936fd..dbbda3540 100644 --- a/apps/graphql/test/queries/cloud_queries_test.exs +++ b/apps/graphql/test/queries/cloud_queries_test.exs @@ -2,6 +2,24 @@ defmodule GraphQl.CloudQueriesTest do use Core.SchemaCase, async: true import GraphQl.TestHelpers + describe "cloudSettings" do + test "it can list supported regions" do + {:ok, %{data: %{"cloudSettings" => settings}}} = run_query(""" + query { + cloudSettings { + regions { + shared { aws } + dedicated { aws } + } + } + } + """, %{}, %{current_user: insert(:user)}) + + refute Enum.empty?(settings["regions"]["shared"]["aws"]) + refute Enum.empty?(settings["regions"]["dedicated"]["aws"]) + end + end + describe "consoleInstances" do test "it can fetch the cloud instances in your account" do user = insert(:user) diff --git a/mix.lock b/mix.lock index 7bbfb31b8..5364e6b4f 100644 --- a/mix.lock +++ b/mix.lock @@ -76,6 +76,7 @@ "guardian": {:hex, :guardian, "1.2.1", "bdc8dd3dbf0fb7216cb6f91c11831faa1a64d39cdaed9a611e37f2413e584983", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "723fc404edfb7bd5cba4cd83329b352037f102aa97468f44e58ac7f47c136a98"}, "hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"}, "hammer": {:hex, :hammer, "6.1.0", "f263e3c3e9946bd410ea0336b2abe0cb6260af4afb3a221e1027540706e76c55", [:make, :mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b47e415a562a6d072392deabcd58090d8a41182cf9044cdd6b0d0faaaf68ba57"}, + "hammer_plug": {:hex, :hammer_plug, "3.0.0", "7b1d000021e3ccc92cc6405c5537d3ec22f8f8f1274a1ae9351a8d98c32a2803", [:mix], [{:hammer, "~> 6.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "35275f98f887bef8d84a8e0a3021ba1cd14d0e15c11221f6f8c833a3d43f35d8"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, diff --git a/schema/schema.graphql b/schema/schema.graphql index 06a0a8f2b..eee8c72ac 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -209,6 +209,8 @@ type RootQueryType { "Get a list of clusters owned by the current account." clusters(after: String, first: Int, before: String, last: Int): ClusterConnection + cloudSettings: PluralCloudSettings + consoleInstance(id: ID!): ConsoleInstance consoleInstances(after: String, first: Int, before: String, last: Int): ConsoleInstanceConnection @@ -219,7 +221,7 @@ type RootQueryType { } type RootMutationType { - login(email: String!, password: String!, deviceToken: String, captcha: String): User + login(email: String!, password: String!, deviceToken: String, captcha: String!): User deviceLogin: DeviceLogin @@ -644,6 +646,19 @@ type ConsoleInstance { updatedAt: DateTime } +type PluralCloudSettings { + regions: PluralCloudRegions +} + +type PluralCloudRegions { + shared: CloudRegions! + dedicated: CloudRegions! +} + +type CloudRegions { + aws: [String] +} + type ConsoleInstanceConnection { pageInfo: PageInfo! edges: [ConsoleInstanceEdge] diff --git a/www/src/generated/graphql.ts b/www/src/generated/graphql.ts index 449c62cb7..b624ea407 100644 --- a/www/src/generated/graphql.ts +++ b/www/src/generated/graphql.ts @@ -385,6 +385,11 @@ export enum CloudProvider { Aws = 'AWS' } +export type CloudRegions = { + __typename?: 'CloudRegions'; + aws?: Maybe>>; +}; + export type CloudShell = { __typename?: 'CloudShell'; aesKey: Scalars['String']['output']; @@ -2208,6 +2213,17 @@ export type PlatformSubscriptionLineItems = { quantity: Scalars['Int']['output']; }; +export type PluralCloudRegions = { + __typename?: 'PluralCloudRegions'; + dedicated: CloudRegions; + shared: CloudRegions; +}; + +export type PluralCloudSettings = { + __typename?: 'PluralCloudSettings'; + regions?: Maybe; +}; + export type PluralConfiguration = { __typename?: 'PluralConfiguration'; gitCommit?: Maybe; @@ -3408,7 +3424,7 @@ export type RootMutationTypeLinkPublisherArgs = { export type RootMutationTypeLoginArgs = { - captcha?: InputMaybe; + captcha: Scalars['String']['input']; deviceToken?: InputMaybe; email: Scalars['String']['input']; password: Scalars['String']['input']; @@ -3733,6 +3749,7 @@ export type RootQueryType = { charts?: Maybe; chat?: Maybe; closure?: Maybe>>; + cloudSettings?: Maybe; /** Get a cluster by its ID. */ cluster?: Maybe; /** Get a list of clusters owned by the current account. */ @@ -6022,7 +6039,7 @@ export type LoginMutationVariables = Exact<{ email: Scalars['String']['input']; password: Scalars['String']['input']; deviceToken?: InputMaybe; - captcha?: InputMaybe; + captcha: Scalars['String']['input']; }>; @@ -10789,7 +10806,7 @@ export type DevLoginMutationHookResult = ReturnType; export type DevLoginMutationResult = Apollo.MutationResult; export type DevLoginMutationOptions = Apollo.BaseMutationOptions; export const LoginDocument = gql` - mutation Login($email: String!, $password: String!, $deviceToken: String, $captcha: String) { + mutation Login($email: String!, $password: String!, $deviceToken: String, $captcha: String!) { login( email: $email password: $password diff --git a/www/src/graph/users.graphql b/www/src/graph/users.graphql index fd8103447..88484cffb 100644 --- a/www/src/graph/users.graphql +++ b/www/src/graph/users.graphql @@ -239,7 +239,7 @@ mutation Login( $email: String! $password: String! $deviceToken: String - $captcha: String + $captcha: String! ) { login( email: $email