diff --git a/.github/codespellrc b/.github/codespellrc index f9b97187c..ecedbc568 100644 --- a/.github/codespellrc +++ b/.github/codespellrc @@ -1,3 +1,3 @@ [codespell] -skip = ./**/*.svg,./elixir/deps,./**/*.min.js,./kotlin/android/app/build,./kotlin/android/build,./e2e/pnpm-lock.yaml,./website/.next,./website/pnpm-lock.yaml,./rust/connlib/tunnel/testcases,./rust/gui-client/dist,./rust/target,Cargo.lock,./website/docs/reference/api/*.mdx,./**/erl_crash.dump,./cover,./vendor,*.json,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./_build,*.cast,./**/proptest-regressions +skip = ./elixir/apps/domain/lib/domain/name_generator.ex,./**/*.svg,./elixir/deps,./**/*.min.js,./kotlin/android/app/build,./kotlin/android/build,./e2e/pnpm-lock.yaml,./website/.next,./website/pnpm-lock.yaml,./rust/connlib/tunnel/testcases,./rust/gui-client/dist,./rust/target,Cargo.lock,./website/docs/reference/api/*.mdx,./**/erl_crash.dump,./cover,./vendor,*.json,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./_build,*.cast,./**/proptest-regressions ignore-words-list = optin,crate,keypair,keypairs,iif,statics,wee,anull,commitish,inout,fo,superceded diff --git a/docker-compose.yml b/docker-compose.yml index 3aa8639f3..96c192526 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -79,7 +79,7 @@ services: DATABASE_USER: postgres DATABASE_PASSWORD: postgres # Auth - AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta,jumpcloud" + AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta,jumpcloud,mock" # Secrets TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" @@ -151,7 +151,7 @@ services: DATABASE_USER: postgres DATABASE_PASSWORD: postgres # Auth - AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta,jumpcloud" + AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta,jumpcloud,mock" # Secrets TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" @@ -215,7 +215,7 @@ services: DATABASE_USER: postgres DATABASE_PASSWORD: postgres # Auth - AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta,jumpcloud" + AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta,jumpcloud,mock" # Secrets TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" @@ -283,7 +283,7 @@ services: DATABASE_USER: postgres DATABASE_PASSWORD: postgres # Auth - AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta,jumpcloud" + AUTH_PROVIDER_ADAPTERS: "email,openid_connect,userpass,token,google_workspace,microsoft_entra,okta,jumpcloud,mock" # Secrets TOKENS_KEY_BASE: "5OVYJ83AcoQcPmdKNksuBhJFBhjHD1uUa9mDOHV/6EIdBQ6pXksIhkVeWIzFk5S2" TOKENS_SALT: "t01wa0K4lUd7mKa0HAtZdE+jFOPDDej2" diff --git a/elixir/apps/domain/lib/domain/auth/adapters.ex b/elixir/apps/domain/lib/domain/auth/adapters.ex index 8d6967409..f31013abb 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters.ex @@ -9,6 +9,7 @@ defmodule Domain.Auth.Adapters do microsoft_entra: Domain.Auth.Adapters.MicrosoftEntra, okta: Domain.Auth.Adapters.Okta, jumpcloud: Domain.Auth.Adapters.JumpCloud, + mock: Domain.Auth.Adapters.Mock, userpass: Domain.Auth.Adapters.UserPass } diff --git a/elixir/apps/domain/lib/domain/auth/adapters/mock.ex b/elixir/apps/domain/lib/domain/auth/adapters/mock.ex new file mode 100644 index 000000000..04f256464 --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/mock.ex @@ -0,0 +1,68 @@ +defmodule Domain.Auth.Adapters.Mock do + use Supervisor + alias Domain.Auth.{Provider, Adapter} + alias Domain.Auth.Adapters.OpenIDConnect + alias Domain.Auth.Adapters.Mock + require Logger + + @behaviour Adapter + + def start_link(_init_arg) do + Supervisor.start_link(__MODULE__, nil, name: __MODULE__) + end + + @impl true + def init(_init_arg) do + children = [ + # Background Jobs + Mock.Jobs.SyncDirectory + ] + + Supervisor.init(children, strategy: :one_for_one) + end + + @impl true + def capabilities do + [ + provisioners: [:custom], + default_provisioner: :custom, + parent_adapter: :openid_connect + ] + end + + @impl true + def identity_changeset(%Provider{} = _provider, %Ecto.Changeset{} = changeset) do + changeset + |> Domain.Repo.Changeset.trim_change(:email) + |> Domain.Repo.Changeset.trim_change(:provider_identifier) + |> Domain.Repo.Changeset.copy_change(:provider_virtual_state, :provider_state) + |> Ecto.Changeset.put_change(:provider_virtual_state, %{}) + end + + @impl true + def provider_changeset(%Ecto.Changeset{} = changeset) do + changeset + |> Domain.Repo.Changeset.cast_polymorphic_embed(:adapter_config, + required: true, + with: fn current_attrs, attrs -> + Ecto.embedded_load(Mock.Settings, current_attrs, :json) + |> Mock.Settings.Changeset.changeset(attrs) + end + ) + end + + @impl true + def ensure_provisioned(%Provider{} = provider) do + {:ok, provider} + end + + @impl true + def ensure_deprovisioned(%Provider{} = provider) do + {:ok, provider} + end + + @impl true + def sign_out(provider, identity, redirect_url) do + OpenIDConnect.sign_out(provider, identity, redirect_url) + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/mock/jobs/sync_directory.ex b/elixir/apps/domain/lib/domain/auth/adapters/mock/jobs/sync_directory.ex new file mode 100644 index 000000000..edc2a5ea0 --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/mock/jobs/sync_directory.ex @@ -0,0 +1,73 @@ +defmodule Domain.Auth.Adapters.Mock.Jobs.SyncDirectory do + use Domain.Jobs.Job, + otp_app: :domain, + every: :timer.minutes(1), + executor: Domain.Jobs.Executors.Concurrent + + alias Domain.Auth.Adapter.OpenIDConnect.DirectorySync + require Logger + require OpenTelemetry.Tracer + + @task_supervisor __MODULE__.TaskSupervisor + + @impl true + def state(_config) do + {:ok, pid} = Task.Supervisor.start_link(name: @task_supervisor) + {:ok, %{task_supervisor: pid}} + end + + @impl true + def execute(%{task_supervisor: pid}) do + DirectorySync.sync_providers(__MODULE__, :mock, pid) + end + + def gather_provider_data(provider, _task_supervisor_pid) do + num_groups = provider.adapter_config["num_groups"] + num_actors = provider.adapter_config["num_actors"] + max_actors_per_group = provider.adapter_config["max_actors_per_group"] + + identities_attrs = + 1..num_actors + |> Enum.map(fn i -> + first_name = Domain.NameGenerator.generate_first_name() + last_name = Domain.NameGenerator.generate_last_name() + + %{ + "provider_identifier" => "U:#{i}", + "provider_state" => %{ + "userinfo" => %{ + "email" => "#{String.downcase(first_name)}@example.com" + } + }, + "actor" => %{ + "type" => :account_user, + "name" => "#{first_name} #{last_name}" + } + } + end) + + actor_groups_attrs = + 1..num_groups + |> Enum.map(fn i -> + group_name = Domain.NameGenerator.generate() + + %{ + "name" => "Group:#{group_name}", + "provider_identifier" => "G:#{i}" + } + end) + + membership_tuples = + Enum.flat_map(1..num_groups, fn i -> + group = Enum.at(actor_groups_attrs, i - 1) + num_members = :rand.uniform(max_actors_per_group) + identities = Enum.take_random(identities_attrs, num_members) + + Enum.map(identities, fn identity -> + {group["provider_identifier"], identity["provider_identifier"]} + end) + end) + + {:ok, {identities_attrs, actor_groups_attrs, membership_tuples}} + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/mock/settings.ex b/elixir/apps/domain/lib/domain/auth/adapters/mock/settings.ex new file mode 100644 index 000000000..410b1c24a --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/mock/settings.ex @@ -0,0 +1,15 @@ +defmodule Domain.Auth.Adapters.Mock.Settings do + use Domain, :schema + + @primary_key false + embedded_schema do + # Number of actors to generate + field :num_actors, :integer, default: 500 + + # Number of groups to generate + field :num_groups, :integer, default: 2_500 + + # Max number of actors per group; will determine the number of memberships + field :max_actors_per_group, :integer, default: 25 + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/mock/settings/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/mock/settings/changeset.ex new file mode 100644 index 000000000..fa44f3795 --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/mock/settings/changeset.ex @@ -0,0 +1,12 @@ +defmodule Domain.Auth.Adapters.Mock.Settings.Changeset do + use Domain, :changeset + alias Domain.Auth.Adapters.Mock.Settings + + @fields ~w[max_actors_per_group num_actors num_groups]a + + def changeset(%Settings{} = settings, attrs) do + settings + |> cast(attrs, @fields) + |> validate_required(@fields) + end +end diff --git a/elixir/apps/domain/lib/domain/auth/provider.ex b/elixir/apps/domain/lib/domain/auth/provider.ex index 52bcc83d9..c8d247c40 100644 --- a/elixir/apps/domain/lib/domain/auth/provider.ex +++ b/elixir/apps/domain/lib/domain/auth/provider.ex @@ -5,7 +5,8 @@ defmodule Domain.Auth.Provider do field :name, :string field :adapter, Ecto.Enum, - values: ~w[email openid_connect google_workspace microsoft_entra okta jumpcloud userpass]a + values: + ~w[email openid_connect google_workspace microsoft_entra okta jumpcloud mock userpass]a field :provisioner, Ecto.Enum, values: ~w[manual just_in_time custom]a field :adapter_config, :map, redact: true diff --git a/elixir/apps/domain/lib/domain/config/definitions.ex b/elixir/apps/domain/lib/domain/config/definitions.ex index 31a19100a..60210e802 100644 --- a/elixir/apps/domain/lib/domain/config/definitions.ex +++ b/elixir/apps/domain/lib/domain/config/definitions.ex @@ -463,6 +463,7 @@ defmodule Domain.Config.Definitions do microsoft_entra okta jumpcloud + mock userpass token ]a)}, @@ -473,6 +474,7 @@ defmodule Domain.Config.Definitions do microsoft_entra okta jumpcloud + mock token ]a ) diff --git a/elixir/apps/domain/lib/domain/name_generator.ex b/elixir/apps/domain/lib/domain/name_generator.ex index e5016fe18..76e1083f1 100644 --- a/elixir/apps/domain/lib/domain/name_generator.ex +++ b/elixir/apps/domain/lib/domain/name_generator.ex @@ -156,6 +156,93 @@ defmodule Domain.NameGenerator do view vision vitality volume voyage warranty wave wealth welfare will wisdom work workforce workshop world yield zone ) + @first_names ~w( + Aaron Abby Abner Abram Ace Ada Adaline Adam Adan Addison Adela Adele Adeline Adrian + Adriana Afton Agatha Agnes Aidan Aileen Aimee Ainsley Aisha Ajax Akira Al Alaina + Alan Alana Alanna Albert Alberta Alden Aldo Alec Alecia Alejandra Alek Alessandra Alex + Alexa Alexander Alexandra Alexis Alfie Alfred Ali Alice Alicia Alina Alisha Alison + Alissa Allie Alma Alvin Alyssa Amara Amari Amanda Amber Amelia Amelie Amina Amira + Amos Amy Anabel Anastasia Anderson Andre Andrea Andres Andrew Angel Angela Angelica + Angelina Angelique Angie Anika Anita Ann Anna Annabelle Anne Annette Annie Anthony + Antonio April Arabella Archer Archie Arden Arely Ariel Aria Arianna Arjun Arlene + Arlo Armando Arnold Arthur Arturo Asa Ashley Ashton Barbara Barrett Barry Beatrice + Beau Beckett Becky Bella Benjamin Bennett Benny Bernard Beth Bethany Bianca Bill Billie + Blake Blanca Bob Bobby Bonnie Boris Boyd Brad Bradley Brady Brandon Brandi Brandy + Breanna Brenda Brent Bret Brett Brian Briana Brianna Bridget Britt Brittany Brock Brody + Brooke Bruce Bruno Bryan Bryant Bryce Bryn Caitlin Caleb Callie Calvin Camila Camille + Candace Cara Carina Carla Carlee Carley Carmen Carol Carolina Caroline Carrie Carson + Carter Casey Cassandra Catherine Cathy Cecilia Cedric Celeste Cesar Chad Charles + Charlie Charlotte Chase Chelsea Chloe Chris Christian Christina Christine + Christopher Daisy Dakota Dale Dallas Damian Damon Dan Dana Daniel Daniela Danielle + Danny Darius Darlene Darren Dave David Dawn Dean Deborah Declan Dee Deirdre Delia + Dennis Derek Desiree Devin Diana Diane Diego Dillon Dominic Don Donald Donna Doris + Dorothy Doug Douglas Earl Easton Eddie Eden Edgar Edith Edward Edwin Elena Eli Elias + Elijah Elise Elizabeth Ella Elle Ellen Elliot Elliott Elsie Ember Emerson Emery Emily + Emma Enrique Eric Erica Erin Esme Faith Felix Fiona Flora Floyd Frances Francesca + Francisco Frank Franklin Fred Frederick Freya Faye Finn Forrest Fergus Farrah Fabian + Gabriel Gabriela Gail Gage Gale Gavin Gemma Gene Genesis Geoffrey George Georgia + Gerald Geraldine Gerardo Gia Gianna Gideon Gilbert Gina Giovanna Giovanni Giselle + Grace Gracie Graham Grant Greg Gregory Greta Griffin Hailey Hank Hannah Harley Harold + Harper Harrison Harry Hazel Heidi Helen Helena Henry Herbert Holly Hope Howard + Hugh Hugo Hunter Ian Ibrahim Ida Imani Imogen India Ingrid Irena Irene Iris Isaac + Isabel Isabella Isabelle Isaiah Isaias Isadora Israel Ivan Ivy Jack Jackie Jackson + Jacob Jade Jaden Jaime Jake James Jamie Jan Jana Jane Janet Janelle Janice Jared + Jasmine Jason Jasper Javier Jay Jayden Jean Jeanette Jeffrey Jenna Jennifer Jeremy + Jerome Jerry Jesse Jessica Jill Jim Jo Joan Joann Joanne Jodie Joe Joel John Johnny + Jordan Joseph Josephine Josh Joshua Josie Kaitlyn Kaleb Kallie Kamal Kamila Kara Karen + Kari Karla Kasey Kate Katelyn Katherine Kathleen Kathryn Katie Kay Kayla Keegan + Keira Keith Kelly Kelvin Ken Kendall Kenneth Kent Kerry Kevin Kiara Kieran Kim + Kimberly King Kinley Kirk Kirsten Kit Kody Kyle Lacey Laila Lamar Lana Lance Landon + Lara Larry Laura Lauren Laurie Lawrence Leah Lee Leila Lena Leo Leon Leonard + Leonardo Leslie Lester Lexi Liam Lila Lily Lincoln Linda Lindsay Lisa Livia Lloyd + Logan Lola Lonnie Lora Lorna Louis Louise Lucas Lucia Lucian Lucy Luis Luke Lydia + Lyle Lynn Lyric Liana Mackenzie Madison + ) + @last_names ~w( + Smith Johnson Williams Brown Jones Garcia Miller Davis Rodriguez Martinez Hernandez + Lopez Gonzalez Wilson Anderson Thomas Taylor Moore Jackson Martin Lee Perez Thompson + White Harris Sanchez Clark Ramirez Lewis Robinson Walker Young Allen King Wright Scott + Torres Nguyen Hill Flores Green Adams Nelson Baker Hall Rivera Campbell Mitchell Carter + Roberts Gomez Phillips Evans Turner Diaz Parker Cruz Edwards Collins Reyes Stewart + Morris Morales Murphy Cook Rogers Gutierrez Ortiz Morgan Cooper Peterson Bailey Reed + Kelly Howard Ramos Kim Cox Ward Richardson Watson Brooks Chavez Wood James Bennett Gray + Mendoza Ruiz Hughes Price Alvarez Castillo Sanders Patel Myers Long Ross Foster + Jimenez Powell Jenkins Perry Russell Sullivan Bell Coleman Butler Henderson Barnes + Gonzales Fisher Vasquez Simmons Romero Jordan Patterson Alexander Hamilton Graham Reynolds + Griffin Wallace West Cole Hayes Bryant Herrera Gibson Ellis Tran Medina Freeman Wells + Webb Simpson Stevens Tucker Porter Hunter Hicks Crawford Henry Boyd Mason Munoz Kennedy + Warren Dixon Bradley Lawson Fuller Burke Santos Mills Armstrong Webster Chapman Lane + Shelton Nichols Gardner Payne Kelley Ferguson Ortega Ramsey Howe Wolfe Newman Lowe Olson + Manning Moran Erickson Donovan Moss Russo Ford Finley Francis Vega Soto Steele + Barker Sharp Schmidt Norton McDonald Mendez Bush Gill Delgado Andrade Matthews Austin + Walsh Pierce Haynes Lyons Ray Bates Schneider Palmer Riley Figueroa Burton Dunn Little + Klein Day Fields Curry Peters Becker Warner Wagner Stevenson Benson Bradford Marshall + Norris Sutton Frazier Murray Brewer Daniels Cross Burns Sims Davidson Barrett Watkins + Baldwin Graves Conrad Todd Berry Jensen Elliott Wilkinson Franklin Harper McCarthy + Owen Spencer Fitzgerald McKinney Cummings Meyers Banks Potter Arnold Harding Guzman + Owens Rowe Lambert Jennings Forbes Ingram Pugh McBride Shaw Velazquez McKenzie Drake + Howell Foley Patrick Carr Andrews O'Neill Glover Hanson Harvey Levine Swanson Douglas + Mercado Nash Boone Maldonado McCormick Cortez Doyle Parsons Morrow Carpenter Watts + McGuire Vaughn Bender Kramer French Chambers Le Benjamin McDaniel Oneal Livingston + Cohen Pearson Petersen Pope Serrano Fischer Campos Hardy Love Garner Cabrera Molina + Bridges Wilkerson Atkinson Orozco Sweeney Gross Casey Lynch Mack Holloway Barton Rojas + Church England Suarez Wise Maddox Khan Zhang Yu Su Fuentes Salinas Cervantes Browning + Hayward McMahon Cannon Rocha Reilly Aguirre Farmer Berg Conley Odom Rush McFarland + Baird Hogan Valdez Kirby Choi Yang Chang Stein Bernard Merrill English Kirk Barry + Cantu Preston Crosby Whitaker Durham Branch Gillespie Franco Santiago Mays Contreras + Monroe McIntosh Buck Heath Simon Duran York McCullough Burnett Henson Petty + Fincher Clay Small Browne Dotson Acevedo Parrish Peck Davila Collier Robles Pitts + McKee Salazar Herman Cline Pace Holden Guerra Davenport Singleton Horn Moody + Valentine Wilkins Charles McKinley House Santana Gilmore Quintana Benton Reece Stout + Bauer Lamb Osborne Randall Combs Ayers Atkins Jefferson Parra Mayer Pineda Duke + Conway McClain Rosales Burch Leon Guerrero Key Logan Rios Shepherd Bullock Horton + Patton Zuniga Delacruz Donaldson Levy Huber Frost Mullen Rasmussen Vance Buckley + Hendrix Sheppard Glass Shearer Dickerson Nielsen Joyner Skinner McLaughlin Padilla + McGee Parks Mccall Workman Dudley Rosario Whitley Christiansen Lowery Booth Spears + Navarro Snyder Rosenthal Donnelly Beard McIntyre Joyce Connelly Blevins Hodge Melton + Pruitt Melendez Calderon Roman Sharpe Meadows Zamora Fitzpatrick Booker Velez + McFadden Hyatt Chandler Roberson + ) def generate do "#{Enum.random(@adjectives)}-#{Enum.random(@nouns)}" @@ -165,4 +252,12 @@ defmodule Domain.NameGenerator do generate() |> String.replace(~r/-/, "_") end + + def generate_first_name do + Enum.random(@first_names) + end + + def generate_last_name do + Enum.random(@last_names) + end end diff --git a/elixir/apps/domain/priv/repo/seeds.exs b/elixir/apps/domain/priv/repo/seeds.exs index f0eb82197..a9d37b0a9 100644 --- a/elixir/apps/domain/priv/repo/seeds.exs +++ b/elixir/apps/domain/priv/repo/seeds.exs @@ -1,1089 +1,1196 @@ -alias Domain.{Repo, Accounts, Auth, Actors, Relays, Gateways, Resources, Policies, Flows, Tokens} - -# Seeds can be run both with MIX_ENV=prod and MIX_ENV=test, for test env we don't have -# an adapter configured and creation of email provider will fail, so we will override it here. -System.put_env("OUTBOUND_EMAIL_ADAPTER", "Elixir.Swoosh.Adapters.Mailgun") - -# This function is used to update fields if STATIC_SEEDS is set, -# which helps with static docker-compose environment for local development. -maybe_repo_update = fn resource, values -> - if System.get_env("STATIC_SEEDS") == "true" do - Ecto.Changeset.change(resource, values) - |> Repo.update!() - else - resource - end -end - -{:ok, account} = - Accounts.create_account(%{ - name: "Firezone Account", - slug: "firezone" - }) - -account -|> Ecto.Changeset.change( - features: %{ - flow_activities: true, - policy_conditions: true, - multi_site_resources: true, - traffic_filters: true, - self_hosted_relays: true, - idp_sync: true, - rest_api: true, - internet_resource: true +defmodule Domain.Repo.Seeds do + @moduledoc """ + Seeds the database with initial data. + """ + alias Domain.{ + Repo, + Accounts, + Auth, + Actors, + Relays, + Gateways, + Resources, + Policies, + Flows, + Tokens } -) -|> Repo.update!() -account = - maybe_repo_update.(account, - id: "c89bcc8c-9392-4dae-a40d-888aef6d28e0", - metadata: %{ - stripe: %{ - customer_id: "cus_PZKIfcHB6SSBA4", - subscription_id: "sub_1OkGm2ADeNU9NGxvbrCCw6m3", - product_name: "Enterprise", - billing_email: "fin@firez.one", - support_type: "email" - } - }, - limits: %{ - users_count: 15, - monthly_active_users_count: 10, - service_accounts_count: 10, - gateway_groups_count: 3, - account_admin_users_count: 5 - } - ) + def seed do + # Seeds can be run both with MIX_ENV=prod and MIX_ENV=test, for test env we don't have + # an adapter configured and creation of email provider will fail, so we will override it here. + System.put_env("OUTBOUND_EMAIL_ADAPTER", "Elixir.Swoosh.Adapters.Mailgun") -{:ok, other_account} = - Accounts.create_account(%{ - name: "Other Corp Account", - slug: "not_firezone" - }) + # Ensure seeds are deterministic + :rand.seed(:exsss, {1, 2, 3}) -other_account = maybe_repo_update.(other_account, id: "9b9290bf-e1bc-4dd3-b401-511908262690") + # This function is used to update fields if STATIC_SEEDS is set, + # which helps with static docker-compose environment for local development. + maybe_repo_update = fn resource, values -> + if System.get_env("STATIC_SEEDS") == "true" do + Ecto.Changeset.change(resource, values) + |> Repo.update!() + else + resource + end + end -IO.puts("Created accounts: ") - -for item <- [account, other_account] do - IO.puts(" #{item.id}: #{item.name}") -end - -IO.puts("") - -{:ok, internet_gateway_group} = - Gateways.create_internet_group(account) - -{:ok, other_internet_gateway_group} = - Gateways.create_internet_group(other_account) - -Domain.Resources.create_internet_resource(account, internet_gateway_group) -Domain.Resources.create_internet_resource(other_account, other_internet_gateway_group) - -IO.puts("") - -{:ok, everyone_group} = - Domain.Actors.create_managed_group(account, %{ - name: "Everyone", - membership_rules: [%{operator: true}] - }) - -{:ok, _everyone_group} = - Domain.Actors.create_managed_group(other_account, %{ - name: "Everyone", - membership_rules: [%{operator: true}] - }) - -{:ok, email_provider} = - Auth.create_provider(account, %{ - name: "Email", - adapter: :email, - adapter_config: %{} - }) - -{:ok, oidc_provider} = - Auth.create_provider(account, %{ - name: "OIDC", - adapter: :openid_connect, - adapter_config: %{ - "client_id" => "CLIENT_ID", - "client_secret" => "CLIENT_SECRET", - "response_type" => "code", - "scope" => "openid email name groups", - "discovery_document_uri" => "https://common.auth0.com/.well-known/openid-configuration" - } - }) - -{:ok, userpass_provider} = - Auth.create_provider(account, %{ - name: "UserPass", - adapter: :userpass, - adapter_config: %{} - }) - -{:ok, _other_email_provider} = - Auth.create_provider(other_account, %{ - name: "email", - adapter: :email, - adapter_config: %{} - }) - -{:ok, other_userpass_provider} = - Auth.create_provider(other_account, %{ - name: "UserPass", - adapter: :userpass, - adapter_config: %{} - }) - -unprivileged_actor_email = "firezone-unprivileged-1@localhost.local" -admin_actor_email = "firezone@localhost.local" - -{:ok, unprivileged_actor} = - Actors.create_actor(account, %{ - type: :account_user, - name: "Firezone Unprivileged" - }) - -other_actors = - for i <- 1..10 do - {:ok, actor} = - Actors.create_actor(account, %{ - type: :account_user, - name: "Firezone Unprivileged #{i}" + {:ok, account} = + Accounts.create_account(%{ + name: "Firezone Account", + slug: "firezone" }) - actor - end + account = + account + |> Ecto.Changeset.change( + features: %{ + flow_activities: true, + policy_conditions: true, + multi_site_resources: true, + traffic_filters: true, + self_hosted_relays: true, + idp_sync: true, + rest_api: true, + internet_resource: true + } + ) + |> Repo.update!() -{:ok, admin_actor} = - Actors.create_actor(account, %{ - type: :account_admin_user, - name: "Firezone Admin" - }) + account = + maybe_repo_update.(account, + id: "c89bcc8c-9392-4dae-a40d-888aef6d28e0", + metadata: %{ + stripe: %{ + customer_id: "cus_PZKIfcHB6SSBA4", + subscription_id: "sub_1OkGm2ADeNU9NGxvbrCCw6m3", + product_name: "Enterprise", + billing_email: "fin@firez.one", + support_type: "email" + } + }, + limits: %{ + users_count: 15, + monthly_active_users_count: 10, + service_accounts_count: 10, + gateway_groups_count: 3, + account_admin_users_count: 5 + } + ) -{:ok, service_account_actor} = - Actors.create_actor(account, %{ - "type" => :service_account, - "name" => "Backup Manager" - }) + {:ok, other_account} = + Accounts.create_account(%{ + name: "Other Corp Account", + slug: "not_firezone" + }) -{:ok, unprivileged_actor_email_identity} = - Auth.create_identity(unprivileged_actor, email_provider, %{ - provider_identifier: unprivileged_actor_email, - provider_identifier_confirmation: unprivileged_actor_email - }) + other_account = maybe_repo_update.(other_account, id: "9b9290bf-e1bc-4dd3-b401-511908262690") -{:ok, unprivileged_actor_userpass_identity} = - Auth.create_identity(unprivileged_actor, userpass_provider, %{ - provider_identifier: unprivileged_actor_email, - provider_virtual_state: %{ - "password" => "Firezone1234", - "password_confirmation" => "Firezone1234" - } - }) + IO.puts("Created accounts: ") -_unprivileged_actor_userpass_identity = - maybe_repo_update.(unprivileged_actor_userpass_identity, - id: "7da7d1cd-111c-44a7-b5ac-4027b9d230e5" - ) + for item <- [account, other_account] do + IO.puts(" #{item.id}: #{item.name}") + end -{:ok, admin_actor_email_identity} = - Auth.create_identity(admin_actor, email_provider, %{ - provider_identifier: admin_actor_email, - provider_identifier_confirmation: admin_actor_email - }) + IO.puts("") -{:ok, _admin_actor_userpass_identity} = - Auth.create_identity(admin_actor, userpass_provider, %{ - provider_identifier: admin_actor_email, - provider_virtual_state: %{ - "password" => "Firezone1234", - "password_confirmation" => "Firezone1234" - } - }) + {:ok, internet_gateway_group} = + Gateways.create_internet_group(account) -{:ok, admin_actor_oidc_identity} = - Auth.create_identity(admin_actor, oidc_provider, %{ - provider_identifier: admin_actor_email, - provider_identifier_confirmation: admin_actor_email - }) + {:ok, other_internet_gateway_group} = + Gateways.create_internet_group(other_account) -admin_actor_oidc_identity -|> Ecto.Changeset.change( - created_by: :provider, - provider_id: oidc_provider.id, - provider_identifier: admin_actor_email, - provider_state: %{"claims" => %{"email" => admin_actor_email, "group" => "users"}} -) -|> Repo.update!() + Domain.Resources.create_internet_resource(account, internet_gateway_group) + Domain.Resources.create_internet_resource(other_account, other_internet_gateway_group) -for actor <- other_actors do - email = "user-#{System.unique_integer([:positive, :monotonic])}@localhost.local" + IO.puts("") - {:ok, identity} = - Auth.create_identity(actor, oidc_provider, %{ - provider_identifier: email, - provider_identifier_confirmation: email - }) + {:ok, everyone_group} = + Domain.Actors.create_managed_group(account, %{ + name: "Everyone", + membership_rules: [%{operator: true}] + }) - identity = - identity + {:ok, _everyone_group} = + Domain.Actors.create_managed_group(other_account, %{ + name: "Everyone", + membership_rules: [%{operator: true}] + }) + + {:ok, email_provider} = + Auth.create_provider(account, %{ + name: "Email", + adapter: :email, + adapter_config: %{} + }) + + {:ok, oidc_provider} = + Auth.create_provider(account, %{ + name: "OIDC", + adapter: :openid_connect, + adapter_config: %{ + "client_id" => "CLIENT_ID", + "client_secret" => "CLIENT_SECRET", + "response_type" => "code", + "scope" => "openid email name groups", + "discovery_document_uri" => "https://common.auth0.com/.well-known/openid-configuration" + } + }) + + {:ok, _mock_provider} = + Auth.create_provider(account, %{ + name: "Mock", + adapter: :mock, + adapter_config: %{} + }) + + {:ok, userpass_provider} = + Auth.create_provider(account, %{ + name: "UserPass", + adapter: :userpass, + adapter_config: %{} + }) + + {:ok, _other_email_provider} = + Auth.create_provider(other_account, %{ + name: "email", + adapter: :email, + adapter_config: %{} + }) + + {:ok, other_userpass_provider} = + Auth.create_provider(other_account, %{ + name: "UserPass", + adapter: :userpass, + adapter_config: %{} + }) + + unprivileged_actor_email = "firezone-unprivileged-1@localhost.local" + admin_actor_email = "firezone@localhost.local" + + {:ok, unprivileged_actor} = + Actors.create_actor(account, %{ + type: :account_user, + name: "Firezone Unprivileged" + }) + + other_actors = + for i <- 1..10 do + {:ok, actor} = + Actors.create_actor(account, %{ + type: :account_user, + name: "Firezone Unprivileged #{i}" + }) + + actor + end + + {:ok, admin_actor} = + Actors.create_actor(account, %{ + type: :account_admin_user, + name: "Firezone Admin" + }) + + {:ok, service_account_actor} = + Actors.create_actor(account, %{ + "type" => :service_account, + "name" => "Backup Manager" + }) + + {:ok, unprivileged_actor_email_identity} = + Auth.create_identity(unprivileged_actor, email_provider, %{ + provider_identifier: unprivileged_actor_email, + provider_identifier_confirmation: unprivileged_actor_email + }) + + {:ok, unprivileged_actor_userpass_identity} = + Auth.create_identity(unprivileged_actor, userpass_provider, %{ + provider_identifier: unprivileged_actor_email, + provider_virtual_state: %{ + "password" => "Firezone1234", + "password_confirmation" => "Firezone1234" + } + }) + + _unprivileged_actor_userpass_identity = + maybe_repo_update.(unprivileged_actor_userpass_identity, + id: "7da7d1cd-111c-44a7-b5ac-4027b9d230e5" + ) + + {:ok, admin_actor_email_identity} = + Auth.create_identity(admin_actor, email_provider, %{ + provider_identifier: admin_actor_email, + provider_identifier_confirmation: admin_actor_email + }) + + {:ok, _admin_actor_userpass_identity} = + Auth.create_identity(admin_actor, userpass_provider, %{ + provider_identifier: admin_actor_email, + provider_virtual_state: %{ + "password" => "Firezone1234", + "password_confirmation" => "Firezone1234" + } + }) + + {:ok, admin_actor_oidc_identity} = + Auth.create_identity(admin_actor, oidc_provider, %{ + provider_identifier: admin_actor_email, + provider_identifier_confirmation: admin_actor_email + }) + + admin_actor_oidc_identity |> Ecto.Changeset.change( created_by: :provider, provider_id: oidc_provider.id, - provider_identifier: email, - provider_state: %{"claims" => %{"email" => email, "group" => "users"}} + provider_identifier: admin_actor_email, + provider_state: %{"claims" => %{"email" => admin_actor_email, "group" => "users"}} ) |> Repo.update!() - context = %Auth.Context{ - type: :browser, - user_agent: "Windows/10.0.22631 seeds/1", - remote_ip: {172, 28, 0, 100}, - remote_ip_location_region: "UA", - remote_ip_location_city: "Kyiv", - remote_ip_location_lat: 50.4333, - remote_ip_location_lon: 30.5167 - } + for actor <- other_actors do + email = "user-#{System.unique_integer([:positive, :monotonic])}@localhost.local" - {:ok, token} = - Auth.create_token(identity, context, "n", nil) + {:ok, identity} = + Auth.create_identity(actor, oidc_provider, %{ + provider_identifier: email, + provider_identifier_confirmation: email + }) - {:ok, subject} = Auth.build_subject(token, context) + identity = + identity + |> Ecto.Changeset.change( + created_by: :provider, + provider_id: oidc_provider.id, + provider_identifier: email, + provider_state: %{"claims" => %{"email" => email, "group" => "users"}} + ) + |> Repo.update!() - count = Enum.random([1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 240]) + context = %Auth.Context{ + type: :browser, + user_agent: "Windows/10.0.22631 seeds/1", + remote_ip: {172, 28, 0, 100}, + remote_ip_location_region: "UA", + remote_ip_location_city: "Kyiv", + remote_ip_location_lat: 50.4333, + remote_ip_location_lon: 30.5167 + } - for i <- 0..count do - user_agent = - Enum.random([ - "iOS/12.7 (iPhone) connlib/1.5.0", - "Android/14 connlib/1.4.1", - "Windows/10.0.22631 connlib/1.3.412", - "Ubuntu/22.4.0 connlib/1.2.2" - ]) + {:ok, token} = + Auth.create_token(identity, context, "n", nil) - client_name = String.split(user_agent, "/") |> List.first() + {:ok, subject} = Auth.build_subject(token, context) - {:ok, _client} = + count = Enum.random([1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 240]) + + for i <- 0..count do + user_agent = + Enum.random([ + "iOS/12.7 (iPhone) connlib/1.5.0", + "Android/14 connlib/1.4.1", + "Windows/10.0.22631 connlib/1.3.412", + "Ubuntu/22.4.0 connlib/1.2.2" + ]) + + client_name = String.split(user_agent, "/") |> List.first() + + {:ok, _client} = + Domain.Clients.upsert_client( + %{ + name: "My #{client_name} #{i}", + external_id: Ecto.UUID.generate(), + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), + identifier_for_vendor: Ecto.UUID.generate() + }, + %{ + subject + | context: %{subject.context | user_agent: user_agent} + } + ) + end + end + + # Other Account Users + other_unprivileged_actor_email = "other-unprivileged-1@localhost.local" + other_admin_actor_email = "other@localhost.local" + + {:ok, other_unprivileged_actor} = + Actors.create_actor(other_account, %{ + type: :account_user, + name: "Other Unprivileged" + }) + + {:ok, other_admin_actor} = + Actors.create_actor(other_account, %{ + type: :account_admin_user, + name: "Other Admin" + }) + + {:ok, _other_unprivileged_actor_userpass_identity} = + Auth.create_identity(other_unprivileged_actor, other_userpass_provider, %{ + provider_identifier: other_unprivileged_actor_email, + provider_virtual_state: %{ + "password" => "Firezone1234", + "password_confirmation" => "Firezone1234" + } + }) + + {:ok, _other_admin_actor_userpass_identity} = + Auth.create_identity(other_admin_actor, other_userpass_provider, %{ + provider_identifier: other_admin_actor_email, + provider_virtual_state: %{ + "password" => "Firezone1234", + "password_confirmation" => "Firezone1234" + } + }) + + unprivileged_actor_context = %Auth.Context{ + type: :browser, + user_agent: "iOS/18.1.0 connlib/1.3.5", + remote_ip: {172, 28, 0, 100}, + remote_ip_location_region: "UA", + remote_ip_location_city: "Kyiv", + remote_ip_location_lat: 50.4333, + remote_ip_location_lon: 30.5167 + } + + nonce = "n" + + {:ok, unprivileged_actor_token} = + Auth.create_token(unprivileged_actor_email_identity, unprivileged_actor_context, nonce, nil) + + {:ok, unprivileged_subject} = + Auth.build_subject(unprivileged_actor_token, unprivileged_actor_context) + + admin_actor_context = %Auth.Context{ + type: :browser, + user_agent: "Mac OS/14.1.2 connlib/1.2.1", + remote_ip: {100, 64, 100, 58}, + remote_ip_location_region: "UA", + remote_ip_location_city: "Kyiv", + remote_ip_location_lat: 50.4333, + remote_ip_location_lon: 30.5167 + } + + {:ok, admin_actor_token} = + Auth.create_token(admin_actor_email_identity, admin_actor_context, nonce, nil) + + {:ok, admin_subject} = + Auth.build_subject(admin_actor_token, admin_actor_context) + + {:ok, service_account_actor_encoded_token} = + Auth.create_service_account_token( + service_account_actor, + %{ + "name" => "tok-#{Ecto.UUID.generate()}", + "expires_at" => DateTime.utc_now() |> DateTime.add(365, :day) + }, + admin_subject + ) + + {:ok, unprivileged_actor_email_identity} = + Domain.Auth.Adapters.Email.request_sign_in_token( + unprivileged_actor_email_identity, + unprivileged_actor_context + ) + + unprivileged_actor_email_token = + unprivileged_actor_email_identity.provider_virtual_state.nonce <> + unprivileged_actor_email_identity.provider_virtual_state.fragment + + {:ok, admin_actor_email_identity} = + Domain.Auth.Adapters.Email.request_sign_in_token( + admin_actor_email_identity, + admin_actor_context + ) + + admin_actor_email_token = + admin_actor_email_identity.provider_virtual_state.nonce <> + admin_actor_email_identity.provider_virtual_state.fragment + + IO.puts("Created users: ") + + for {type, login, password, email_token} <- [ + {unprivileged_actor.type, unprivileged_actor_email, "Firezone1234", + unprivileged_actor_email_token}, + {admin_actor.type, admin_actor_email, "Firezone1234", admin_actor_email_token} + ] do + IO.puts( + " #{login}, #{type}, password: #{password}, email token: #{email_token} (exp in 15m)" + ) + end + + IO.puts(" #{service_account_actor.name} token: #{service_account_actor_encoded_token}") + IO.puts("") + + {:ok, user_iphone} = Domain.Clients.upsert_client( %{ - name: "My #{client_name} #{i}", + name: "FZ User iPhone", external_id: Ecto.UUID.generate(), public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), - identifier_for_vendor: Ecto.UUID.generate() + identifier_for_vendor: "APPL-#{Ecto.UUID.generate()}" }, %{ - subject - | context: %{subject.context | user_agent: user_agent} + unprivileged_subject + | context: %{ + unprivileged_subject.context + | user_agent: "iOS/12.7 (iPhone) connlib/0.7.412" + } } ) - end -end -# Other Account Users -other_unprivileged_actor_email = "other-unprivileged-1@localhost.local" -other_admin_actor_email = "other@localhost.local" - -{:ok, other_unprivileged_actor} = - Actors.create_actor(other_account, %{ - type: :account_user, - name: "Other Unprivileged" - }) - -{:ok, other_admin_actor} = - Actors.create_actor(other_account, %{ - type: :account_admin_user, - name: "Other Admin" - }) - -{:ok, _other_unprivileged_actor_userpass_identity} = - Auth.create_identity(other_unprivileged_actor, other_userpass_provider, %{ - provider_identifier: other_unprivileged_actor_email, - provider_virtual_state: %{ - "password" => "Firezone1234", - "password_confirmation" => "Firezone1234" - } - }) - -{:ok, _other_admin_actor_userpass_identity} = - Auth.create_identity(other_admin_actor, other_userpass_provider, %{ - provider_identifier: other_admin_actor_email, - provider_virtual_state: %{ - "password" => "Firezone1234", - "password_confirmation" => "Firezone1234" - } - }) - -unprivileged_actor_context = %Auth.Context{ - type: :browser, - user_agent: "iOS/18.1.0 connlib/1.3.5", - remote_ip: {172, 28, 0, 100}, - remote_ip_location_region: "UA", - remote_ip_location_city: "Kyiv", - remote_ip_location_lat: 50.4333, - remote_ip_location_lon: 30.5167 -} - -nonce = "n" - -{:ok, unprivileged_actor_token} = - Auth.create_token(unprivileged_actor_email_identity, unprivileged_actor_context, nonce, nil) - -{:ok, unprivileged_subject} = - Auth.build_subject(unprivileged_actor_token, unprivileged_actor_context) - -admin_actor_context = %Auth.Context{ - type: :browser, - user_agent: "Mac OS/14.1.2 connlib/1.2.1", - remote_ip: {100, 64, 100, 58}, - remote_ip_location_region: "UA", - remote_ip_location_city: "Kyiv", - remote_ip_location_lat: 50.4333, - remote_ip_location_lon: 30.5167 -} - -{:ok, admin_actor_token} = - Auth.create_token(admin_actor_email_identity, admin_actor_context, nonce, nil) - -{:ok, admin_subject} = - Auth.build_subject(admin_actor_token, admin_actor_context) - -{:ok, service_account_actor_encoded_token} = - Auth.create_service_account_token( - service_account_actor, - %{ - "name" => "tok-#{Ecto.UUID.generate()}", - "expires_at" => DateTime.utc_now() |> DateTime.add(365, :day) - }, - admin_subject - ) - -{:ok, unprivileged_actor_email_identity} = - Domain.Auth.Adapters.Email.request_sign_in_token( - unprivileged_actor_email_identity, - unprivileged_actor_context - ) - -unprivileged_actor_email_token = - unprivileged_actor_email_identity.provider_virtual_state.nonce <> - unprivileged_actor_email_identity.provider_virtual_state.fragment - -{:ok, admin_actor_email_identity} = - Domain.Auth.Adapters.Email.request_sign_in_token( - admin_actor_email_identity, - admin_actor_context - ) - -admin_actor_email_token = - admin_actor_email_identity.provider_virtual_state.nonce <> - admin_actor_email_identity.provider_virtual_state.fragment - -IO.puts("Created users: ") - -for {type, login, password, email_token} <- [ - {unprivileged_actor.type, unprivileged_actor_email, "Firezone1234", - unprivileged_actor_email_token}, - {admin_actor.type, admin_actor_email, "Firezone1234", admin_actor_email_token} - ] do - IO.puts(" #{login}, #{type}, password: #{password}, email token: #{email_token} (exp in 15m)") -end - -IO.puts(" #{service_account_actor.name} token: #{service_account_actor_encoded_token}") -IO.puts("") - -{:ok, user_iphone} = - Domain.Clients.upsert_client( - %{ - name: "FZ User iPhone", - external_id: Ecto.UUID.generate(), - public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), - identifier_for_vendor: "APPL-#{Ecto.UUID.generate()}" - }, - %{ - unprivileged_subject - | context: %{unprivileged_subject.context | user_agent: "iOS/12.7 (iPhone) connlib/0.7.412"} - } - ) - -{:ok, _user_android_phone} = - Domain.Clients.upsert_client( - %{ - name: "FZ User Android", - external_id: Ecto.UUID.generate(), - public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), - identifier_for_vendor: "GOOG-#{Ecto.UUID.generate()}" - }, - %{ - unprivileged_subject - | context: %{unprivileged_subject.context | user_agent: "Android/14 connlib/0.7.412"} - } - ) - -{:ok, _user_windows_laptop} = - Domain.Clients.upsert_client( - %{ - name: "FZ User Surface", - external_id: Ecto.UUID.generate(), - public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), - device_uuid: "WIN-#{Ecto.UUID.generate()}" - }, - %{ - unprivileged_subject - | context: %{ - unprivileged_subject.context - | user_agent: "Windows/10.0.22631 connlib/0.7.412" + {:ok, _user_android_phone} = + Domain.Clients.upsert_client( + %{ + name: "FZ User Android", + external_id: Ecto.UUID.generate(), + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), + identifier_for_vendor: "GOOG-#{Ecto.UUID.generate()}" + }, + %{ + unprivileged_subject + | context: %{unprivileged_subject.context | user_agent: "Android/14 connlib/0.7.412"} } - } - ) + ) -{:ok, _user_linux_laptop} = - Domain.Clients.upsert_client( - %{ - name: "FZ User Rendering Station", - external_id: Ecto.UUID.generate(), - public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), - device_uuid: "UB-#{Ecto.UUID.generate()}" - }, - %{ - unprivileged_subject - | context: %{unprivileged_subject.context | user_agent: "Ubuntu/22.4.0 connlib/0.7.412"} - } - ) + {:ok, _user_windows_laptop} = + Domain.Clients.upsert_client( + %{ + name: "FZ User Surface", + external_id: Ecto.UUID.generate(), + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), + device_uuid: "WIN-#{Ecto.UUID.generate()}" + }, + %{ + unprivileged_subject + | context: %{ + unprivileged_subject.context + | user_agent: "Windows/10.0.22631 connlib/0.7.412" + } + } + ) -{:ok, _admin_iphone} = - Domain.Clients.upsert_client( - %{ - name: "FZ Admin Laptop", - external_id: Ecto.UUID.generate(), - public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), - device_serial: "FVFHF246Q72Z", - device_uuid: "#{Ecto.UUID.generate()}" - }, - %{ - admin_subject - | context: %{admin_subject.context | user_agent: "Mac OS/14.5 connlib/0.7.412"} - } - ) + {:ok, _user_linux_laptop} = + Domain.Clients.upsert_client( + %{ + name: "FZ User Rendering Station", + external_id: Ecto.UUID.generate(), + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), + device_uuid: "UB-#{Ecto.UUID.generate()}" + }, + %{ + unprivileged_subject + | context: %{unprivileged_subject.context | user_agent: "Ubuntu/22.4.0 connlib/0.7.412"} + } + ) -IO.puts("Clients created") -IO.puts("") + {:ok, _admin_iphone} = + Domain.Clients.upsert_client( + %{ + name: "FZ Admin Laptop", + external_id: Ecto.UUID.generate(), + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(), + device_serial: "FVFHF246Q72Z", + device_uuid: "#{Ecto.UUID.generate()}" + }, + %{ + admin_subject + | context: %{admin_subject.context | user_agent: "Mac OS/14.5 connlib/0.7.412"} + } + ) -IO.puts("Created Actor Groups: ") + IO.puts("Clients created") + IO.puts("") -for _i <- 1..300 do - Actors.create_group( - %{name: Domain.Accounts.generate_unique_slug(), type: :static}, - admin_subject - ) -end + IO.puts("Created Actor Groups: ") -{:ok, eng_group} = Actors.create_group(%{name: "Engineering", type: :static}, admin_subject) -{:ok, finance_group} = Actors.create_group(%{name: "Finance", type: :static}, admin_subject) - -{:ok, synced_group} = - Actors.create_group(%{name: "Group:Synced Group with long name", type: :static}, admin_subject) - -for group <- [eng_group, finance_group, synced_group] do - IO.puts(" Name: #{group.name} ID: #{group.id}") -end - -eng_group -|> Repo.preload(:memberships) -|> Actors.update_group( - %{memberships: [%{actor_id: admin_subject.actor.id}]}, - admin_subject -) - -finance_group -|> Repo.preload(:memberships) -|> Actors.update_group( - %{memberships: [%{actor_id: unprivileged_subject.actor.id}]}, - admin_subject -) - -synced_group -|> Repo.preload(:memberships) -|> Actors.update_group( - %{ - memberships: [ - %{actor_id: admin_subject.actor.id}, - %{actor_id: unprivileged_subject.actor.id} + # Collect all actors for this account + all_actors = [ + unprivileged_actor, + admin_actor, + service_account_actor | other_actors ] - }, - admin_subject -) -synced_group -|> Ecto.Changeset.change( - created_by: :provider, - provider_id: oidc_provider.id, - provider_identifier: "dummy_oidc_group_id" -) -|> Repo.update!() + actor_ids = Enum.map(all_actors, & &1.id) + # Total number of actors + max_members = length(actor_ids) -oidc_provider -|> Ecto.Changeset.change(last_synced_at: DateTime.utc_now()) -|> Repo.update!() + # Create groups in chunks and collect their IDs + group_ids = + 1..10_000 + # Process in chunks to manage memory + |> Enum.chunk_every(1000) + |> Enum.flat_map(fn chunk -> + group_attrs = + Enum.map(chunk, fn i -> + %{ + name: "#{Domain.Accounts.generate_unique_slug()}-#{i}", + type: :static, + provider_id: oidc_provider.id, + provider_identifier: Ecto.UUID.generate(), + created_by: :provider, + account_id: admin_subject.account.id, + inserted_at: DateTime.utc_now(), + updated_at: DateTime.utc_now() + } + end) -for name <- [ - "Group:gcp-logging-viewers", - "Group:gcp-security-admins", - "Group:gcp-organization-admins", - "OU:Admins", - "OU:Product", - "Group:Engineering", - "Group:gcp-developers" - ] do - {:ok, group} = Actors.create_group(%{name: name, type: :static}, admin_subject) + {_, inserted_groups} = + Repo.insert_all( + Domain.Actors.Group, + group_attrs, + returning: [:id] + ) - group - |> Repo.preload(:memberships) - |> Actors.update_group( - %{memberships: [%{actor_id: admin_subject.actor.id}]}, - admin_subject - ) -end + Enum.map(inserted_groups, & &1.id) + end) -IO.puts("") + # Create memberships + memberships = + group_ids + |> Enum.chunk_every(1000) + |> Enum.flat_map(fn group_chunk -> + Enum.flat_map(group_chunk, fn group_id -> + # Determine random number of members (1 to max_members) + num_members = :rand.uniform(max_members) -{:ok, global_relay_group} = - Relays.create_global_group(%{name: "fz-global-relays"}) + # Select random actor IDs + member_ids = + actor_ids + # Uses seeded random + |> Enum.shuffle() + |> Enum.take(num_members) -{:ok, global_relay_group_token} = - Tokens.create_token(%{ - "type" => :relay_group, - "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), - "relay_group_id" => global_relay_group.id - }) + # Create membership attributes + Enum.map(member_ids, fn actor_id -> + %{ + group_id: group_id, + actor_id: actor_id, + account_id: admin_subject.account.id + } + end) + end) + end) -global_relay_group_token = - global_relay_group_token - |> maybe_repo_update.( - id: "e82fcdc1-057a-4015-b90b-3b18f0f28053", - secret_salt: "lZWUdgh-syLGVDsZEu_29A", - secret_fragment: "C14NGA87EJRR03G4QPR07A9C6G784TSSTHSF4TI5T0GD8D6L0VRG====", - secret_hash: "c3c9a031ae98f111ada642fddae546de4e16ceb85214ab4f1c9d0de1fc472797" - ) + # Bulk insert memberships + memberships + |> Enum.chunk_every(1000) + |> Enum.each(fn chunk -> + Repo.insert_all(Domain.Actors.Membership, chunk) + end) -global_relay_group_encoded_token = Tokens.encode_fragment!(global_relay_group_token) + {:ok, eng_group} = Actors.create_group(%{name: "Engineering", type: :static}, admin_subject) + {:ok, finance_group} = Actors.create_group(%{name: "Finance", type: :static}, admin_subject) -IO.puts("Created global relay groups:") -IO.puts(" #{global_relay_group.name} token: #{global_relay_group_encoded_token}") + {:ok, synced_group} = + Actors.create_group( + %{name: "Group:Synced Group with long name", type: :static}, + admin_subject + ) -IO.puts("") + for group <- [eng_group, finance_group, synced_group] do + IO.puts(" Name: #{group.name} ID: #{group.id}") + end -relay_context = %Auth.Context{ - type: :relay_group, - user_agent: "Ubuntu/14.04 connlib/0.7.412", - remote_ip: {100, 64, 100, 58} -} - -{:ok, global_relay} = - Relays.upsert_relay( - global_relay_group, - global_relay_group_token, - %{ - ipv4: {189, 172, 72, 111}, - ipv6: {0, 0, 0, 0, 0, 0, 0, 1} - }, - relay_context - ) - -for i <- 1..5 do - {:ok, _global_relay} = - Relays.upsert_relay( - global_relay_group, - global_relay_group_token, - %{ - ipv4: {189, 172, 72, 111 + i}, - ipv6: {0, 0, 0, 0, 0, 0, 0, i} - }, - %{relay_context | remote_ip: %Postgrex.INET{address: {189, 172, 72, 111 + i}}} + eng_group + |> Repo.preload(:memberships) + |> Actors.update_group( + %{memberships: [%{actor_id: admin_subject.actor.id}]}, + admin_subject ) -end -IO.puts("Created global relays:") -IO.puts(" Group #{global_relay_group.name}:") -IO.puts(" IPv4: #{global_relay.ipv4} IPv6: #{global_relay.ipv6}") -IO.puts("") + finance_group + |> Repo.preload(:memberships) + |> Actors.update_group( + %{memberships: [%{actor_id: unprivileged_subject.actor.id}]}, + admin_subject + ) -relay_group = - account - |> Relays.Group.Changeset.create(%{name: "mycorp-aws-relays"}, admin_subject) - |> Repo.insert!() + synced_group + |> Repo.preload(:memberships) + |> Actors.update_group( + %{ + memberships: [ + %{actor_id: admin_subject.actor.id}, + %{actor_id: unprivileged_subject.actor.id} + ] + }, + admin_subject + ) -{:ok, relay_group_token} = - Tokens.create_token(%{ - "type" => :relay_group, - "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), - "account_id" => admin_subject.account.id, - "relay_group_id" => global_relay_group.id - }) + synced_group + |> Ecto.Changeset.change( + created_by: :provider, + provider_id: oidc_provider.id, + provider_identifier: "dummy_oidc_group_id" + ) + |> Repo.update!() -relay_group_token = - relay_group_token - |> maybe_repo_update.( - id: "549c4107-1492-4f8f-a4ec-a9d2a66d8aa9", - secret_salt: "jaJwcwTRhzQr15SgzTB2LA", - secret_fragment: "PU5AITE1O8VDVNMHMOAC77DIKMOGTDIA672S6G1AB02OS34H5ME0====", - secret_hash: "af133f7efe751ca978ec3e5fadf081ce9ab50138ff52862395858c3d2c11c0c5" - ) + oidc_provider + |> Ecto.Changeset.change(last_synced_at: DateTime.utc_now()) + |> Repo.update!() -relay_group_encoded_token = Tokens.encode_fragment!(relay_group_token) + for name <- [ + "Group:gcp-logging-viewers", + "Group:gcp-security-admins", + "Group:gcp-organization-admins", + "OU:Admins", + "OU:Product", + "Group:Engineering", + "Group:gcp-developers" + ] do + {:ok, group} = Actors.create_group(%{name: name, type: :static}, admin_subject) -IO.puts("Created relay groups:") -IO.puts(" #{relay_group.name} token: #{relay_group_encoded_token}") -IO.puts("") + group + |> Repo.preload(:memberships) + |> Actors.update_group( + %{memberships: [%{actor_id: admin_subject.actor.id}]}, + admin_subject + ) + end -{:ok, relay} = - Relays.upsert_relay( - relay_group, - relay_group_token, - %{ - ipv4: {189, 172, 73, 111}, - ipv6: {0, 0, 0, 0, 0, 0, 0, 1} - }, - %Auth.Context{ + IO.puts("") + + {:ok, global_relay_group} = + Relays.create_global_group(%{name: "fz-global-relays"}) + + {:ok, global_relay_group_token} = + Tokens.create_token(%{ + "type" => :relay_group, + "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), + "relay_group_id" => global_relay_group.id + }) + + global_relay_group_token = + global_relay_group_token + |> maybe_repo_update.( + id: "e82fcdc1-057a-4015-b90b-3b18f0f28053", + secret_salt: "lZWUdgh-syLGVDsZEu_29A", + secret_fragment: "C14NGA87EJRR03G4QPR07A9C6G784TSSTHSF4TI5T0GD8D6L0VRG====", + secret_hash: "c3c9a031ae98f111ada642fddae546de4e16ceb85214ab4f1c9d0de1fc472797" + ) + + global_relay_group_encoded_token = Tokens.encode_fragment!(global_relay_group_token) + + IO.puts("Created global relay groups:") + IO.puts(" #{global_relay_group.name} token: #{global_relay_group_encoded_token}") + + IO.puts("") + + relay_context = %Auth.Context{ type: :relay_group, - user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - remote_ip: %Postgrex.INET{address: {189, 172, 73, 111}} + user_agent: "Ubuntu/14.04 connlib/0.7.412", + remote_ip: {100, 64, 100, 58} } - ) -for i <- 1..5 do - {:ok, _relay} = - Relays.upsert_relay( - relay_group, - relay_group_token, - %{ - ipv4: {189, 172, 73, 111 + i}, - ipv6: {0, 0, 0, 0, 0, 0, 0, i} - }, - %Auth.Context{ - type: :relay_group, - user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - remote_ip: %Postgrex.INET{address: {189, 172, 73, 111}} - } + {:ok, global_relay} = + Relays.upsert_relay( + global_relay_group, + global_relay_group_token, + %{ + ipv4: {189, 172, 72, 111}, + ipv6: {0, 0, 0, 0, 0, 0, 0, 1} + }, + relay_context + ) + + for i <- 1..5 do + {:ok, _global_relay} = + Relays.upsert_relay( + global_relay_group, + global_relay_group_token, + %{ + ipv4: {189, 172, 72, 111 + i}, + ipv6: {0, 0, 0, 0, 0, 0, 0, i} + }, + %{relay_context | remote_ip: %Postgrex.INET{address: {189, 172, 72, 111 + i}}} + ) + end + + IO.puts("Created global relays:") + IO.puts(" Group #{global_relay_group.name}:") + IO.puts(" IPv4: #{global_relay.ipv4} IPv6: #{global_relay.ipv6}") + IO.puts("") + + relay_group = + account + |> Relays.Group.Changeset.create(%{name: "mycorp-aws-relays"}, admin_subject) + |> Repo.insert!() + + {:ok, relay_group_token} = + Tokens.create_token(%{ + "type" => :relay_group, + "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), + "account_id" => admin_subject.account.id, + "relay_group_id" => global_relay_group.id + }) + + relay_group_token = + relay_group_token + |> maybe_repo_update.( + id: "549c4107-1492-4f8f-a4ec-a9d2a66d8aa9", + secret_salt: "jaJwcwTRhzQr15SgzTB2LA", + secret_fragment: "PU5AITE1O8VDVNMHMOAC77DIKMOGTDIA672S6G1AB02OS34H5ME0====", + secret_hash: "af133f7efe751ca978ec3e5fadf081ce9ab50138ff52862395858c3d2c11c0c5" + ) + + relay_group_encoded_token = Tokens.encode_fragment!(relay_group_token) + + IO.puts("Created relay groups:") + IO.puts(" #{relay_group.name} token: #{relay_group_encoded_token}") + IO.puts("") + + {:ok, relay} = + Relays.upsert_relay( + relay_group, + relay_group_token, + %{ + ipv4: {189, 172, 73, 111}, + ipv6: {0, 0, 0, 0, 0, 0, 0, 1} + }, + %Auth.Context{ + type: :relay_group, + user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + remote_ip: %Postgrex.INET{address: {189, 172, 73, 111}} + } + ) + + for i <- 1..5 do + {:ok, _relay} = + Relays.upsert_relay( + relay_group, + relay_group_token, + %{ + ipv4: {189, 172, 73, 111 + i}, + ipv6: {0, 0, 0, 0, 0, 0, 0, i} + }, + %Auth.Context{ + type: :relay_group, + user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + remote_ip: %Postgrex.INET{address: {189, 172, 73, 111}} + } + ) + end + + IO.puts("Created relays:") + IO.puts(" Group #{relay_group.name}:") + IO.puts(" IPv4: #{relay.ipv4} IPv6: #{relay.ipv6}") + IO.puts("") + + gateway_group = + account + |> Gateways.Group.Changeset.create( + %{name: "mycro-aws-gws", tokens: [%{}]}, + admin_subject + ) + |> Repo.insert!() + + {:ok, gateway_group_token} = + Tokens.create_token( + %{ + "type" => :gateway_group, + "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), + "account_id" => admin_subject.account.id, + "gateway_group_id" => gateway_group.id + }, + admin_subject + ) + + gateway_group_token = + gateway_group_token + |> maybe_repo_update.( + id: "2274560b-e97b-45e4-8b34-679c7617e98d", + secret_salt: "uQyisyqrvYIIitMXnSJFKQ", + secret_fragment: "O02L7US2J3VINOMPR9J6IL88QIQP6UO8AQVO6U5IPL0VJC22JGH0====", + secret_hash: "876f20e8d4de25d5ffac40733f280782a7d8097347d77415ab6e4e548f13d2ee" + ) + + gateway_group_encoded_token = Tokens.encode_fragment!(gateway_group_token) + + IO.puts("Created gateway groups:") + IO.puts(" #{gateway_group.name} token: #{gateway_group_encoded_token}") + IO.puts("") + + {:ok, gateway1} = + Gateways.upsert_gateway( + gateway_group, + gateway_group_token, + %{ + external_id: Ecto.UUID.generate(), + name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64() + }, + %Auth.Context{ + type: :gateway_group, + user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}} + } + ) + + {:ok, gateway2} = + Gateways.upsert_gateway( + gateway_group, + gateway_group_token, + %{ + external_id: Ecto.UUID.generate(), + name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64() + }, + %Auth.Context{ + type: :gateway_group, + user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + remote_ip: %Postgrex.INET{address: {164, 112, 78, 62}} + } + ) + + for i <- 1..10 do + {:ok, _gateway} = + Gateways.upsert_gateway( + gateway_group, + gateway_group_token, + %{ + external_id: Ecto.UUID.generate(), + name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", + public_key: :crypto.strong_rand_bytes(32) |> Base.encode64() + }, + %Auth.Context{ + type: :gateway_group, + user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", + remote_ip: %Postgrex.INET{address: {164, 112, 78, 62 + i}} + } + ) + end + + IO.puts("Created gateways:") + gateway_name = "#{gateway_group.name}-#{gateway1.name}" + IO.puts(" #{gateway_name}:") + IO.puts(" External UUID: #{gateway1.external_id}") + IO.puts(" Public Key: #{gateway1.public_key}") + IO.puts(" IPv4: #{gateway1.ipv4} IPv6: #{gateway1.ipv6}") + IO.puts("") + + gateway_name = "#{gateway_group.name}-#{gateway2.name}" + IO.puts(" #{gateway_name}:") + IO.puts(" External UUID: #{gateway1.external_id}") + IO.puts(" Public Key: #{gateway2.public_key}") + IO.puts(" IPv4: #{gateway2.ipv4} IPv6: #{gateway2.ipv6}") + IO.puts("") + + {:ok, dns_google_resource} = + Resources.create_resource( + %{ + type: :dns, + name: "google.com", + address: "google.com", + address_description: "https://google.com/", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [] + }, + admin_subject + ) + + {:ok, firez_one} = + Resources.create_resource( + %{ + type: :dns, + name: "**.firez.one", + address: "**.firez.one", + address_description: "https://firez.one/", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [] + }, + admin_subject + ) + + {:ok, firezone_dev} = + Resources.create_resource( + %{ + type: :dns, + name: "*.firezone.dev", + address: "*.firezone.dev", + address_description: "https://firezone.dev/", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [] + }, + admin_subject + ) + + {:ok, example_dns} = + Resources.create_resource( + %{ + type: :dns, + name: "example.com", + address: "example.com", + address_description: "https://example.com:1234/", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [] + }, + admin_subject + ) + + {:ok, ip6only} = + Resources.create_resource( + %{ + type: :dns, + name: "ip6only", + address: "ip6only.me", + address_description: "https://ip6only.me/", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [] + }, + admin_subject + ) + + {:ok, address_description_null_resource} = + Resources.create_resource( + %{ + type: :dns, + name: "Google", + address: "*.google.com", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [] + }, + admin_subject + ) + + {:ok, dns_gitlab_resource} = + Resources.create_resource( + %{ + type: :dns, + name: "gitlab.mycorp.com", + address: "gitlab.mycorp.com", + address_description: "https://gitlab.mycorp.com/", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [ + %{ports: ["80", "433"], protocol: :tcp}, + %{ports: ["53"], protocol: :udp}, + %{protocol: :icmp} + ] + }, + admin_subject + ) + + {:ok, ip_resource} = + Resources.create_resource( + %{ + type: :ip, + name: "CloudFlare DNS", + address: "1.1.1.1", + address_description: "http://1.1.1.1:3000/", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [ + %{ports: ["80", "433"], protocol: :tcp}, + %{ports: ["53"], protocol: :udp}, + %{protocol: :icmp} + ] + }, + admin_subject + ) + + {:ok, cidr_resource} = + Resources.create_resource( + %{ + type: :cidr, + name: "MyCorp Network", + address: "172.20.0.1/16", + address_description: "172.20.0.1/16", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [] + }, + admin_subject + ) + + {:ok, dns_httpbin_resource} = + Resources.create_resource( + %{ + type: :dns, + name: "**.httpbin", + address: "**.httpbin", + address_description: "http://httpbin/", + connections: [%{gateway_group_id: gateway_group.id}], + filters: [ + %{ports: ["80", "433"], protocol: :tcp}, + %{ports: ["53"], protocol: :udp}, + %{protocol: :icmp} + ] + }, + admin_subject + ) + + IO.puts("Created resources:") + IO.puts(" #{dns_google_resource.address} - DNS - gateways: #{gateway_name}") + IO.puts(" #{address_description_null_resource.address} - DNS - gateways: #{gateway_name}") + IO.puts(" #{dns_gitlab_resource.address} - DNS - gateways: #{gateway_name}") + IO.puts(" #{firez_one.address} - DNS - gateways: #{gateway_name}") + IO.puts(" #{firezone_dev.address} - DNS - gateways: #{gateway_name}") + IO.puts(" #{example_dns.address} - DNS - gateways: #{gateway_name}") + IO.puts(" #{ip_resource.address} - IP - gateways: #{gateway_name}") + IO.puts(" #{cidr_resource.address} - CIDR - gateways: #{gateway_name}") + IO.puts(" #{dns_httpbin_resource.address} - DNS - gateways: #{gateway_name}") + IO.puts("") + + {:ok, _} = + Policies.create_policy( + %{ + name: "All Access To Google", + actor_group_id: everyone_group.id, + resource_id: dns_google_resource.id + }, + admin_subject + ) + + {:ok, _} = + Policies.create_policy( + %{ + name: "All Access To firez.one", + actor_group_id: synced_group.id, + resource_id: firez_one.id + }, + admin_subject + ) + + {:ok, _} = + Policies.create_policy( + %{ + name: "All Access To firez.one", + actor_group_id: everyone_group.id, + resource_id: example_dns.id + }, + admin_subject + ) + + {:ok, _} = + Policies.create_policy( + %{ + name: "All Access To firezone.dev", + actor_group_id: everyone_group.id, + resource_id: firezone_dev.id + }, + admin_subject + ) + + {:ok, _} = + Policies.create_policy( + %{ + name: "All Access To ip6only.me", + actor_group_id: synced_group.id, + resource_id: ip6only.id + }, + admin_subject + ) + + {:ok, _} = + Policies.create_policy( + %{ + name: "All access to Google", + actor_group_id: everyone_group.id, + resource_id: address_description_null_resource.id + }, + admin_subject + ) + + {:ok, _} = + Policies.create_policy( + %{ + name: "Eng Access To Gitlab", + actor_group_id: eng_group.id, + resource_id: dns_gitlab_resource.id + }, + admin_subject + ) + + {:ok, _} = + Policies.create_policy( + %{ + name: "All Access To Network", + actor_group_id: synced_group.id, + resource_id: cidr_resource.id + }, + admin_subject + ) + + {:ok, _} = + Policies.create_policy( + %{ + name: "All Access To dns.httpbin", + actor_group_id: everyone_group.id, + resource_id: dns_httpbin_resource.id + }, + admin_subject + ) + + IO.puts("Policies Created") + IO.puts("") + + {:ok, unprivileged_subject_client_token} = + Auth.create_token( + unprivileged_actor_email_identity, + %{unprivileged_actor_context | type: :client}, + nonce, + nil + ) + + unprivileged_subject_client_token = + maybe_repo_update.(unprivileged_subject_client_token, + id: "7da7d1cd-111c-44a7-b5ac-4027b9d230e5", + secret_salt: "kKKA7dtf3TJk0-1O2D9N1w", + secret_fragment: "AiIy_6pBk-WLeRAPzzkCFXNqIZKWBs2Ddw_2vgIQvFg", + secret_hash: "5c1d6795ea1dd08b6f4fd331eeaffc12032ba171d227f328446f2d26b96437e5" + ) + + IO.puts("Created client tokens:") + + IO.puts( + " #{unprivileged_actor_email} token: #{nonce <> Domain.Tokens.encode_fragment!(unprivileged_subject_client_token)}" ) -end -IO.puts("Created relays:") -IO.puts(" Group #{relay_group.name}:") -IO.puts(" IPv4: #{relay.ipv4} IPv6: #{relay.ipv6}") -IO.puts("") + IO.puts("") -gateway_group = - account - |> Gateways.Group.Changeset.create( - %{name: "mycro-aws-gws", tokens: [%{}]}, - admin_subject - ) - |> Repo.insert!() + {:ok, _resource, flow} = + Flows.authorize_flow( + user_iphone, + gateway1, + cidr_resource.id, + unprivileged_subject + ) -{:ok, gateway_group_token} = - Tokens.create_token( - %{ - "type" => :gateway_group, - "secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32), - "account_id" => admin_subject.account.id, - "gateway_group_id" => gateway_group.id - }, - admin_subject - ) + started_at = + DateTime.utc_now() + |> DateTime.truncate(:second) + |> DateTime.add(5, :minute) -gateway_group_token = - gateway_group_token - |> maybe_repo_update.( - id: "2274560b-e97b-45e4-8b34-679c7617e98d", - secret_salt: "uQyisyqrvYIIitMXnSJFKQ", - secret_fragment: "O02L7US2J3VINOMPR9J6IL88QIQP6UO8AQVO6U5IPL0VJC22JGH0====", - secret_hash: "876f20e8d4de25d5ffac40733f280782a7d8097347d77415ab6e4e548f13d2ee" - ) + {:ok, destination1} = Domain.Types.ProtocolIPPort.cast("tcp://142.250.217.142:443") + {:ok, destination2} = Domain.Types.ProtocolIPPort.cast("udp://142.250.217.142:111") -gateway_group_encoded_token = Tokens.encode_fragment!(gateway_group_token) + random_integer = fn -> + :math.pow(10, 10) + |> round() + |> :rand.uniform() + |> floor() + |> Kernel.-(1) + end -IO.puts("Created gateway groups:") -IO.puts(" #{gateway_group.name} token: #{gateway_group_encoded_token}") -IO.puts("") + activities = + for i <- 1..200 do + offset = i * 15 + started_at = DateTime.add(started_at, offset, :minute) + ended_at = DateTime.add(started_at, 15, :minute) -{:ok, gateway1} = - Gateways.upsert_gateway( - gateway_group, - gateway_group_token, - %{ - external_id: Ecto.UUID.generate(), - name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", - public_key: :crypto.strong_rand_bytes(32) |> Base.encode64() - }, - %Auth.Context{ - type: :gateway_group, - user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}} - } - ) + %{ + window_started_at: started_at, + window_ended_at: ended_at, + destination: Enum.random([destination1, destination2]), + connectivity_type: :direct, + rx_bytes: random_integer.(), + tx_bytes: random_integer.(), + blocked_tx_bytes: 0, + flow_id: flow.id, + account_id: account.id + } + end -{:ok, gateway2} = - Gateways.upsert_gateway( - gateway_group, - gateway_group_token, - %{ - external_id: Ecto.UUID.generate(), - name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", - public_key: :crypto.strong_rand_bytes(32) |> Base.encode64() - }, - %Auth.Context{ - type: :gateway_group, - user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - remote_ip: %Postgrex.INET{address: {164, 112, 78, 62}} - } - ) - -for i <- 1..10 do - {:ok, _gateway} = - Gateways.upsert_gateway( - gateway_group, - gateway_group_token, - %{ - external_id: Ecto.UUID.generate(), - name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}", - public_key: :crypto.strong_rand_bytes(32) |> Base.encode64() - }, - %Auth.Context{ - type: :gateway_group, - user_agent: "iOS/12.7 (iPhone) connlib/0.7.412", - remote_ip: %Postgrex.INET{address: {164, 112, 78, 62 + i}} - } - ) -end - -IO.puts("Created gateways:") -gateway_name = "#{gateway_group.name}-#{gateway1.name}" -IO.puts(" #{gateway_name}:") -IO.puts(" External UUID: #{gateway1.external_id}") -IO.puts(" Public Key: #{gateway1.public_key}") -IO.puts(" IPv4: #{gateway1.ipv4} IPv6: #{gateway1.ipv6}") -IO.puts("") - -gateway_name = "#{gateway_group.name}-#{gateway2.name}" -IO.puts(" #{gateway_name}:") -IO.puts(" External UUID: #{gateway1.external_id}") -IO.puts(" Public Key: #{gateway2.public_key}") -IO.puts(" IPv4: #{gateway2.ipv4} IPv6: #{gateway2.ipv6}") -IO.puts("") - -{:ok, dns_google_resource} = - Resources.create_resource( - %{ - type: :dns, - name: "google.com", - address: "google.com", - address_description: "https://google.com/", - connections: [%{gateway_group_id: gateway_group.id}], - filters: [] - }, - admin_subject - ) - -{:ok, firez_one} = - Resources.create_resource( - %{ - type: :dns, - name: "**.firez.one", - address: "**.firez.one", - address_description: "https://firez.one/", - connections: [%{gateway_group_id: gateway_group.id}], - filters: [] - }, - admin_subject - ) - -{:ok, firezone_dev} = - Resources.create_resource( - %{ - type: :dns, - name: "*.firezone.dev", - address: "*.firezone.dev", - address_description: "https://firezone.dev/", - connections: [%{gateway_group_id: gateway_group.id}], - filters: [] - }, - admin_subject - ) - -{:ok, example_dns} = - Resources.create_resource( - %{ - type: :dns, - name: "example.com", - address: "example.com", - address_description: "https://example.com:1234/", - connections: [%{gateway_group_id: gateway_group.id}], - filters: [] - }, - admin_subject - ) - -{:ok, ip6only} = - Resources.create_resource( - %{ - type: :dns, - name: "ip6only", - address: "ip6only.me", - address_description: "https://ip6only.me/", - connections: [%{gateway_group_id: gateway_group.id}], - filters: [] - }, - admin_subject - ) - -{:ok, address_description_null_resource} = - Resources.create_resource( - %{ - type: :dns, - name: "Google", - address: "*.google.com", - connections: [%{gateway_group_id: gateway_group.id}], - filters: [] - }, - admin_subject - ) - -{:ok, dns_gitlab_resource} = - Resources.create_resource( - %{ - type: :dns, - name: "gitlab.mycorp.com", - address: "gitlab.mycorp.com", - address_description: "https://gitlab.mycorp.com/", - connections: [%{gateway_group_id: gateway_group.id}], - filters: [ - %{ports: ["80", "433"], protocol: :tcp}, - %{ports: ["53"], protocol: :udp}, - %{protocol: :icmp} - ] - }, - admin_subject - ) - -{:ok, ip_resource} = - Resources.create_resource( - %{ - type: :ip, - name: "CloudFlare DNS", - address: "1.1.1.1", - address_description: "http://1.1.1.1:3000/", - connections: [%{gateway_group_id: gateway_group.id}], - filters: [ - %{ports: ["80", "433"], protocol: :tcp}, - %{ports: ["53"], protocol: :udp}, - %{protocol: :icmp} - ] - }, - admin_subject - ) - -{:ok, cidr_resource} = - Resources.create_resource( - %{ - type: :cidr, - name: "MyCorp Network", - address: "172.20.0.1/16", - address_description: "172.20.0.1/16", - connections: [%{gateway_group_id: gateway_group.id}], - filters: [] - }, - admin_subject - ) - -{:ok, dns_httpbin_resource} = - Resources.create_resource( - %{ - type: :dns, - name: "**.httpbin", - address: "**.httpbin", - address_description: "http://httpbin/", - connections: [%{gateway_group_id: gateway_group.id}], - filters: [ - %{ports: ["80", "433"], protocol: :tcp}, - %{ports: ["53"], protocol: :udp}, - %{protocol: :icmp} - ] - }, - admin_subject - ) - -IO.puts("Created resources:") -IO.puts(" #{dns_google_resource.address} - DNS - gateways: #{gateway_name}") -IO.puts(" #{address_description_null_resource.address} - DNS - gateways: #{gateway_name}") -IO.puts(" #{dns_gitlab_resource.address} - DNS - gateways: #{gateway_name}") -IO.puts(" #{firez_one.address} - DNS - gateways: #{gateway_name}") -IO.puts(" #{firezone_dev.address} - DNS - gateways: #{gateway_name}") -IO.puts(" #{example_dns.address} - DNS - gateways: #{gateway_name}") -IO.puts(" #{ip_resource.address} - IP - gateways: #{gateway_name}") -IO.puts(" #{cidr_resource.address} - CIDR - gateways: #{gateway_name}") -IO.puts(" #{dns_httpbin_resource.address} - DNS - gateways: #{gateway_name}") -IO.puts("") - -{:ok, _} = - Policies.create_policy( - %{ - name: "All Access To Google", - actor_group_id: everyone_group.id, - resource_id: dns_google_resource.id - }, - admin_subject - ) - -{:ok, _} = - Policies.create_policy( - %{ - name: "All Access To firez.one", - actor_group_id: synced_group.id, - resource_id: firez_one.id - }, - admin_subject - ) - -{:ok, _} = - Policies.create_policy( - %{ - name: "All Access To firez.one", - actor_group_id: everyone_group.id, - resource_id: example_dns.id - }, - admin_subject - ) - -{:ok, _} = - Policies.create_policy( - %{ - name: "All Access To firezone.dev", - actor_group_id: everyone_group.id, - resource_id: firezone_dev.id - }, - admin_subject - ) - -{:ok, _} = - Policies.create_policy( - %{ - name: "All Access To ip6only.me", - actor_group_id: synced_group.id, - resource_id: ip6only.id - }, - admin_subject - ) - -{:ok, _} = - Policies.create_policy( - %{ - name: "All access to Google", - actor_group_id: everyone_group.id, - resource_id: address_description_null_resource.id - }, - admin_subject - ) - -{:ok, _} = - Policies.create_policy( - %{ - name: "Eng Access To Gitlab", - actor_group_id: eng_group.id, - resource_id: dns_gitlab_resource.id - }, - admin_subject - ) - -{:ok, _} = - Policies.create_policy( - %{ - name: "All Access To Network", - actor_group_id: synced_group.id, - resource_id: cidr_resource.id - }, - admin_subject - ) - -{:ok, _} = - Policies.create_policy( - %{ - name: "All Access To dns.httpbin", - actor_group_id: everyone_group.id, - resource_id: dns_httpbin_resource.id - }, - admin_subject - ) - -IO.puts("Policies Created") -IO.puts("") - -{:ok, unprivileged_subject_client_token} = - Auth.create_token( - unprivileged_actor_email_identity, - %{unprivileged_actor_context | type: :client}, - nonce, - nil - ) - -unprivileged_subject_client_token = - maybe_repo_update.(unprivileged_subject_client_token, - id: "7da7d1cd-111c-44a7-b5ac-4027b9d230e5", - secret_salt: "kKKA7dtf3TJk0-1O2D9N1w", - secret_fragment: "AiIy_6pBk-WLeRAPzzkCFXNqIZKWBs2Ddw_2vgIQvFg", - secret_hash: "5c1d6795ea1dd08b6f4fd331eeaffc12032ba171d227f328446f2d26b96437e5" - ) - -IO.puts("Created client tokens:") - -IO.puts( - " #{unprivileged_actor_email} token: #{nonce <> Domain.Tokens.encode_fragment!(unprivileged_subject_client_token)}" -) - -IO.puts("") - -{:ok, _resource, flow} = - Flows.authorize_flow( - user_iphone, - gateway1, - cidr_resource.id, - unprivileged_subject - ) - -started_at = - DateTime.utc_now() - |> DateTime.truncate(:second) - |> DateTime.add(5, :minute) - -{:ok, destination1} = Domain.Types.ProtocolIPPort.cast("tcp://142.250.217.142:443") -{:ok, destination2} = Domain.Types.ProtocolIPPort.cast("udp://142.250.217.142:111") - -random_integer = fn -> - :math.pow(10, 10) - |> round() - |> :rand.uniform() - |> floor() - |> Kernel.-(1) -end - -activities = - for i <- 1..200 do - offset = i * 15 - started_at = DateTime.add(started_at, offset, :minute) - ended_at = DateTime.add(started_at, 15, :minute) - - %{ - window_started_at: started_at, - window_ended_at: ended_at, - destination: Enum.random([destination1, destination2]), - connectivity_type: :direct, - rx_bytes: random_integer.(), - tx_bytes: random_integer.(), - blocked_tx_bytes: 0, - flow_id: flow.id, - account_id: account.id - } + {:ok, 200} = Flows.upsert_activities(activities) end +end -{:ok, 200} = Flows.upsert_activities(activities) +Domain.Repo.Seeds.seed() diff --git a/elixir/apps/domain/test/domain/auth_test.exs b/elixir/apps/domain/test/domain/auth_test.exs index b9694855f..79ffffc33 100644 --- a/elixir/apps/domain/test/domain/auth_test.exs +++ b/elixir/apps/domain/test/domain/auth_test.exs @@ -14,6 +14,7 @@ defmodule Domain.AuthTest do google_workspace: [enabled: true, sync: true], jumpcloud: [enabled: true, sync: true], microsoft_entra: [enabled: true, sync: true], + mock: [enabled: true, sync: true], okta: [enabled: true, sync: true], openid_connect: [enabled: true, sync: false] ] @@ -24,6 +25,7 @@ defmodule Domain.AuthTest do google_workspace: [enabled: false, sync: true], jumpcloud: [enabled: false, sync: true], microsoft_entra: [enabled: false, sync: true], + mock: [enabled: false, sync: true], okta: [enabled: false, sync: true], openid_connect: [enabled: true, sync: false] ] diff --git a/elixir/apps/domain/test/support/fixtures/auth.ex b/elixir/apps/domain/test/support/fixtures/auth.ex index dba6636b4..f6574ee0e 100644 --- a/elixir/apps/domain/test/support/fixtures/auth.ex +++ b/elixir/apps/domain/test/support/fixtures/auth.ex @@ -43,31 +43,15 @@ defmodule Domain.Fixtures.Auth do "user-#{unique_integer()}@#{String.downcase(name)}.com" end - def random_provider_identifier(%Domain.Auth.Provider{adapter: :openid_connect}) do - Ecto.UUID.generate() - end - - def random_provider_identifier(%Domain.Auth.Provider{adapter: :google_workspace}) do - Ecto.UUID.generate() - end - - def random_provider_identifier(%Domain.Auth.Provider{adapter: :microsoft_entra}) do - Ecto.UUID.generate() - end - - def random_provider_identifier(%Domain.Auth.Provider{adapter: :okta}) do - Ecto.UUID.generate() - end - - def random_provider_identifier(%Domain.Auth.Provider{adapter: :jumpcloud}) do - Ecto.UUID.generate() - end - def random_provider_identifier(%Domain.Auth.Provider{adapter: :userpass, name: name}) do "user-#{unique_integer()}@#{String.downcase(name)}.com" end - def random_workos_org_identifier() do + def random_provider_identifier(%Domain.Auth.Provider{adapter: _other_adapter}) do + Ecto.UUID.generate() + end + + def random_workos_org_identifier do chars = Range.to_list(?A..?Z) ++ Range.to_list(?0..?9) str = for _ <- 1..26, into: "", do: <> "org_#{str}" diff --git a/elixir/apps/web/lib/web/components/core_components.ex b/elixir/apps/web/lib/web/components/core_components.ex index e5bff09ed..3d684bd68 100644 --- a/elixir/apps/web/lib/web/components/core_components.ex +++ b/elixir/apps/web/lib/web/components/core_components.ex @@ -1539,6 +1539,12 @@ defmodule Web.CoreComponents do """ end + def provider_icon(%{adapter: :mock} = assigns) do + ~H""" + <.icon name="hero-command-line" {@rest} /> + """ + end + def provider_icon(%{adapter: :email} = assigns) do ~H""" <.icon name="hero-envelope" {@rest} /> diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex index bf93ff57d..5ebab7f71 100644 --- a/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex @@ -268,6 +268,7 @@ defmodule Web.Settings.IdentityProviders.Components do def adapter_name(:microsoft_entra), do: "Microsoft Entra" def adapter_name(:okta), do: "Okta" def adapter_name(:jumpcloud), do: "JumpCloud" + def adapter_name(:mock), do: "Mock" def adapter_name(:openid_connect), do: "OpenID Connect" def view_provider(account, %{adapter: adapter} = provider) @@ -289,6 +290,9 @@ defmodule Web.Settings.IdentityProviders.Components do def view_provider(account, %{adapter: :jumpcloud} = provider), do: ~p"/#{account}/settings/identity_providers/jumpcloud/#{provider}" + def view_provider(account, %{adapter: :mock} = provider), + do: ~p"/#{account}/settings/identity_providers/mock/#{provider}" + def sync_status(%{provider: %{provisioner: :custom}} = assigns) do ~H"""
diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/mock/components.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/mock/components.ex new file mode 100644 index 000000000..a27426cb7 --- /dev/null +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/mock/components.ex @@ -0,0 +1,75 @@ +defmodule Web.Settings.IdentityProviders.Mock.Components do + use Web, :component_library + + def provider_form(assigns) do + ~H""" +
+ <.form for={@form} phx-change={:change} phx-submit={:submit}> + <.step> + <:title>Configure the Mock adapter + <:content> + <.base_error form={@form} field={:base} /> + +
+
+ <.input + label="Name" + autocomplete="off" + field={@form[:name]} + placeholder="Name this identity provider" + required + /> +

+ A friendly name for this identity provider. +

+
+ + <.inputs_for :let={adapter_config_form} field={@form[:adapter_config]}> +
+ <.input + label="Number of actors" + autocomplete="off" + field={adapter_config_form[:num_actors]} + required + /> +

+ The total number of actors to randomly generate. +

+
+ +
+ <.input + label="Number of groups" + autocomplete="off" + field={adapter_config_form[:num_groups]} + required + /> +

+ The total number of groups to randomly generate. +

+
+ +
+ <.input + label="Max actors per group" + autocomplete="off" + field={adapter_config_form[:max_actors_per_group]} + required + /> +

+ The maximum number of actors per group. A random number of actors will be assigned to each group, up to this limit. +

+
+ +
+ + <.submit_button> + Save Identity Provider + + + + +
+ """ + end +end diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/mock/edit.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/mock/edit.ex new file mode 100644 index 000000000..05d98682d --- /dev/null +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/mock/edit.ex @@ -0,0 +1,66 @@ +defmodule Web.Settings.IdentityProviders.Mock.Edit do + use Web, :live_view + import Web.Settings.IdentityProviders.Mock.Components + alias Domain.Auth + + def mount(%{"provider_id" => provider_id}, _session, socket) do + with {:ok, provider} <- Domain.Auth.fetch_provider_by_id(provider_id, socket.assigns.subject) do + changeset = Auth.change_provider(provider) + + socket = + assign(socket, + provider: provider, + form: to_form(changeset), + page_title: "Edit #{provider.name}" + ) + + {:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]} + else + {:error, _reason} -> raise Web.LiveErrors.NotFoundError + end + end + + def render(assigns) do + ~H""" + <.breadcrumbs account={@account}> + <.breadcrumb path={~p"/#{@account}/settings/identity_providers"}> + Identity Providers Settings + + <.breadcrumb path={~p"/#{@account}/settings/identity_providers/mock/#{@form.data}/edit"}> + Edit + + + <.section> + <:title> + Edit Identity Provider {@form.data.name} + + <:content> + <.provider_form account={@account} id={@form.data.id} form={@form} /> + + + """ + end + + def handle_event("change", %{"provider" => attrs}, socket) do + changeset = + Auth.change_provider(socket.assigns.provider, attrs) + |> Map.put(:action, :insert) + + {:noreply, assign(socket, form: to_form(changeset))} + end + + def handle_event("submit", %{"provider" => attrs}, socket) do + with {:ok, provider} <- + Auth.update_provider(socket.assigns.provider, attrs, socket.assigns.subject) do + socket = + push_navigate(socket, + to: ~p"/#{socket.assigns.account.id}/settings/identity_providers/mock/#{provider}" + ) + + {:noreply, socket} + else + {:error, changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end +end diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/mock/new.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/mock/new.ex new file mode 100644 index 000000000..f37c43184 --- /dev/null +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/mock/new.ex @@ -0,0 +1,83 @@ +defmodule Web.Settings.IdentityProviders.Mock.New do + use Web, :live_view + import Web.Settings.IdentityProviders.Mock.Components + alias Domain.Auth + + def mount(_params, _session, socket) do + id = Ecto.UUID.generate() + + changeset = + Auth.new_provider(socket.assigns.account, %{ + name: "Mock", + adapter: :mock, + adapter_config: %{} + }) + + socket = + assign(socket, + id: id, + form: to_form(changeset), + page_title: "New Identity Provider: Mock" + ) + + {:ok, socket, temporary_assigns: [form: %Phoenix.HTML.Form{}]} + end + + def render(assigns) do + ~H""" + <.breadcrumbs account={@account}> + <.breadcrumb path={~p"/#{@account}/settings/identity_providers"}> + Identity Providers Settings + + <.breadcrumb path={~p"/#{@account}/settings/identity_providers/new"}> + Create Identity Provider + + <.breadcrumb path={~p"/#{@account}/settings/identity_providers/mock/new"}> + Mock + + + <.section> + <:title>{@page_title} + <:content> + <.provider_form account={@account} id={@id} form={@form} /> + + + """ + end + + def handle_event("change", %{"provider" => attrs}, socket) do + attrs = Map.put(attrs, "adapter", :mock) + + changeset = + Auth.new_provider(socket.assigns.account, attrs) + |> Map.put(:action, :insert) + + {:noreply, assign(socket, form: to_form(changeset))} + end + + def handle_event("submit", %{"provider" => attrs}, socket) do + attrs = + attrs + |> Map.put("id", socket.assigns.id) + |> Map.put("adapter", :mock) + + with {:ok, provider} <- + Auth.create_provider(socket.assigns.account, attrs, socket.assigns.subject) do + socket = + push_navigate(socket, + to: ~p"/#{socket.assigns.account.id}/settings/identity_providers/mock/#{provider}" + ) + + {:noreply, socket} + else + {:error, changeset} -> + # Here we can have an insert conflict error, which will be returned without embedded fields information, + # this will crash `.inputs_for` component in the template, so we need to handle it here. + new_changeset = + Auth.new_provider(socket.assigns.account, attrs) + |> Map.put(:action, :insert) + + {:noreply, assign(socket, form: to_form(%{new_changeset | errors: changeset.errors}))} + end + end +end diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/mock/show.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/mock/show.ex new file mode 100644 index 000000000..1d29138bb --- /dev/null +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/mock/show.ex @@ -0,0 +1,278 @@ +defmodule Web.Settings.IdentityProviders.Mock.Show do + use Web, :live_view + import Web.Settings.IdentityProviders.Components + alias Domain.{Auth, Actors} + + def mount(%{"provider_id" => provider_id}, _session, socket) do + with {:ok, provider} <- + Auth.fetch_provider_by_id(provider_id, socket.assigns.subject, + preload: [created_by_identity: [:actor]] + ), + {:ok, identities_count_by_provider_id} <- + Auth.fetch_identities_count_grouped_by_provider_id(socket.assigns.subject), + {:ok, groups_count_by_provider_id} <- + Actors.fetch_groups_count_grouped_by_provider_id(socket.assigns.subject) do + safe_to_delete_actors_count = Actors.count_synced_actors_for_provider(provider) + + {:ok, + assign(socket, + provider: provider, + identities_count_by_provider_id: identities_count_by_provider_id, + groups_count_by_provider_id: groups_count_by_provider_id, + safe_to_delete_actors_count: safe_to_delete_actors_count, + page_title: "Identity Provider #{provider.name}" + )} + else + _ -> raise Web.LiveErrors.NotFoundError + end + end + + def render(assigns) do + ~H""" + <.breadcrumbs account={@account}> + <.breadcrumb path={~p"/#{@account}/settings/identity_providers"}> + Identity Providers Settings + + + <.breadcrumb path={~p"/#{@account}/settings/identity_providers/mock/#{@provider}"}> + {@provider.name} + + + + <.section> + <:title> + Identity Provider: {@provider.name} + (disabled) + (deleted) + + <:action :if={is_nil(@provider.deleted_at)}> + <.edit_button navigate={ + ~p"/#{@account}/settings/identity_providers/mock/#{@provider.id}/edit" + }> + Edit + + + <:action :if={is_nil(@provider.deleted_at)}> + <.button_with_confirmation + :if={is_nil(@provider.disabled_at)} + id="disable" + style="warning" + icon="hero-lock-closed" + on_confirm="disable" + > + <:dialog_title>Confirm disabling the Provider + <:dialog_content> + Are you sure you want to disable this Provider? + This will immediately + sign out all Actors who were signed in using this Provider and directory sync will be paused. + + <:dialog_confirm_button> + Disable + + <:dialog_cancel_button> + Cancel + + Disable + + <.button_with_confirmation + :if={not is_nil(@provider.disabled_at)} + id="enable" + style="warning" + confirm_style="primary" + icon="hero-lock-open" + on_confirm="enable" + > + <:dialog_title>Confirm enabling the Provider + <:dialog_content> + Are you sure you want to enable this provider? + + <:dialog_confirm_button> + Enable + + <:dialog_cancel_button> + Cancel + + Enable + + + <:help> +

+ Directory sync is enabled for this provider. Users and groups will be synced every few + minutes on average, but could take longer for very large organizations. +

+

+ <.website_link path="/kb/authenticate/directory-sync"> + Read more + + about directory sync. +

+ + <:content> + <.header> + <:title>Details + + + <.flash_group flash={@flash} /> + + <.flash :if={@safe_to_delete_actors_count > 0} kind={:warning}> + You have {@safe_to_delete_actors_count} Actor(s) that were synced from this provider and do not have any other identities. + <.button_with_confirmation + id="delete_stale_actors" + style="danger" + icon="hero-trash-solid" + on_confirm="delete_stale_actors" + class="mt-4" + > + <:dialog_title>Confirm deletion of stale Actors + <:dialog_content> + Are you sure you want to delete all Actors that were synced synced from this provider and do not have any other identities? + + <:dialog_confirm_button> + Delete Actors + + <:dialog_cancel_button> + Cancel + + Delete Actors + + + +
+ <.vertical_table id="provider"> + <.vertical_table_row> + <:label>Name + <:value>{@provider.name} + + <.vertical_table_row> + <:label>Description + <:value> + The Mock Identity Provider is a test provider that generates random Actors and + Groups using the same sync mechanism as the other directory sync adapters. The + difference is that it performs no actual network requests and generates random + data. + + + <.vertical_table_row> + <:label>Status + <:value> + <.status provider={@provider} /> + + + + <.vertical_table_row> + <:label>Sync Status + <:value> + <.sync_status + account={@account} + provider={@provider} + identities_count_by_provider_id={@identities_count_by_provider_id} + groups_count_by_provider_id={@groups_count_by_provider_id} + /> +
3 and not is_nil(@provider.last_sync_error)) + } + class="w-fit p-3 mt-2 border-l-4 border-red-500 bg-red-100 rounded-md" + > +

+ IdP provider reported an error during the last sync: +

+
+ {@provider.last_sync_error} +
+
+ + + <.vertical_table_row> + <:label>Number of actors to generate + <:value>{@provider.adapter_config["num_actors"]} + + <.vertical_table_row> + <:label>Number of groups to generate + <:value>{@provider.adapter_config["num_groups"]} + + <.vertical_table_row> + <:label>Max number of actors per group + <:value>{@provider.adapter_config["max_actors_per_group"]} + + <.vertical_table_row> + <:label>Created + <:value> + <.created_by account={@account} schema={@provider} /> + + + +
+ + + + <.danger_zone :if={is_nil(@provider.deleted_at)}> + <:action> + <.button_with_confirmation + id="delete_identity_provider" + style="danger" + icon="hero-trash-solid" + on_confirm="delete" + > + <:dialog_title>Confirm deletion of Identity Provider + <:dialog_content> + Are you sure you want to delete this provider? This will remove all + Actors and Groups associated with this provider. + + <:dialog_confirm_button> + Delete Identity Provider + + <:dialog_cancel_button> + Cancel + + Delete Identity Provider + + + + """ + end + + def handle_event("delete", _params, socket) do + {:ok, _provider} = Auth.delete_provider(socket.assigns.provider, socket.assigns.subject) + + {:noreply, + push_navigate(socket, to: ~p"/#{socket.assigns.account}/settings/identity_providers")} + end + + def handle_event("delete_stale_actors", _params, socket) do + :ok = + Actors.delete_stale_synced_actors_for_provider( + socket.assigns.provider, + socket.assigns.subject + ) + + {:noreply, + push_navigate(socket, to: view_provider(socket.assigns.account, socket.assigns.provider))} + end + + def handle_event("enable", _params, socket) do + attrs = %{disabled_at: nil} + {:ok, provider} = Auth.update_provider(socket.assigns.provider, attrs, socket.assigns.subject) + + {:ok, provider} = + Auth.fetch_provider_by_id(provider.id, socket.assigns.subject, + preload: [created_by_identity: [:actor]] + ) + + {:noreply, assign(socket, provider: provider)} + end + + def handle_event("disable", _params, socket) do + attrs = %{disabled_at: DateTime.utc_now()} + {:ok, provider} = Auth.update_provider(socket.assigns.provider, attrs, socket.assigns.subject) + + {:ok, provider} = + Auth.fetch_provider_by_id(provider.id, socket.assigns.subject, + preload: [created_by_identity: [:actor]] + ) + + {:noreply, assign(socket, provider: provider)} + end +end diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/new.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/new.ex index ca11df0c5..94472811e 100644 --- a/elixir/apps/web/lib/web/live/settings/identity_providers/new.ex +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/new.ex @@ -125,6 +125,10 @@ defmodule Web.Settings.IdentityProviders.New do end end + def next_step_path("mock", account) do + ~p"/#{account}/settings/identity_providers/mock/new" + end + def pretty_print_provider(adapter) do case adapter do :openid_connect -> "OpenID Connect" @@ -132,6 +136,7 @@ defmodule Web.Settings.IdentityProviders.New do :microsoft_entra -> "Microsoft EntraID" :okta -> "Okta" :jumpcloud -> "JumpCloud" + :mock -> "Mock" end end end diff --git a/elixir/apps/web/lib/web/router.ex b/elixir/apps/web/lib/web/router.ex index fcbb5ff33..d76605ca6 100644 --- a/elixir/apps/web/lib/web/router.ex +++ b/elixir/apps/web/lib/web/router.ex @@ -280,6 +280,12 @@ defmodule Web.Router do get "/:provider_id/handle_callback", Connect, :handle_idp_callback end + scope "/mock", Mock do + live "/new", New + live "/:provider_id", Show + live "/:provider_id/edit", Edit + end + scope "/system", System do live "/:provider_id", Show end diff --git a/elixir/config/runtime.exs b/elixir/config/runtime.exs index 9d29b11da..10baee89a 100644 --- a/elixir/config/runtime.exs +++ b/elixir/config/runtime.exs @@ -109,6 +109,12 @@ if config_env() == :prod do config :domain, Domain.Auth.Adapters.Okta.Jobs.SyncDirectory, enabled: compile_config!(:background_jobs_enabled) + config :domain, Domain.Auth.Adapters.JumpCloud.Jobs.SyncDirectory, + enabled: compile_config!(:background_jobs_enabled) + + config :domain, Domain.Auth.Adapters.Mock.Jobs.SyncDirectory, + enabled: compile_config!(:background_jobs_enabled) + if web_external_url = compile_config!(:web_external_url) do %{ scheme: web_external_url_scheme,