From a26cd682d9fed88bef4f14cdb6fff1c0107b1257 Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Mon, 7 Oct 2024 11:46:02 -0400 Subject: [PATCH] Implement user-bound oidc providers (#1372) --- apps/core/lib/core/policies/oauth.ex | 11 ++ apps/core/lib/core/schema/oidc_provider.ex | 16 ++- apps/core/lib/core/services/base.ex | 12 +++ apps/core/lib/core/services/oauth.ex | 93 ++++++++++++++++ apps/core/lib/core/services/repositories.ex | 9 -- ...41005171529_add_owner_id_oidc_provider.exs | 11 ++ apps/core/test/services/oauth_test.exs | 75 +++++++++++++ apps/graphql/lib/graphql/resolvers/oauth.ex | 8 +- .../lib/graphql/resolvers/repository.ex | 18 +++- apps/graphql/lib/graphql/schema/oauth.ex | 6 ++ apps/graphql/lib/graphql/schema/repository.ex | 28 +++-- .../mutations/repository_mutation_test.exs | 100 ++++++++++++++++++ .../test/queries/oauth_queries_test.exs | 21 ++++ config/config.exs | 2 + schema/schema.graphql | 27 ++++- www/src/generated/graphql.ts | 37 ++++++- 16 files changed, 445 insertions(+), 29 deletions(-) create mode 100644 apps/core/lib/core/policies/oauth.ex create mode 100644 apps/core/priv/repo/migrations/20241005171529_add_owner_id_oidc_provider.exs diff --git a/apps/core/lib/core/policies/oauth.ex b/apps/core/lib/core/policies/oauth.ex new file mode 100644 index 000000000..02432b9d0 --- /dev/null +++ b/apps/core/lib/core/policies/oauth.ex @@ -0,0 +1,11 @@ +defmodule Core.Policies.OAuth do + use Piazza.Policy + alias Core.Schema.{User, OIDCProvider} + + def can?(%User{id: id}, %OIDCProvider{owner_id: id}, _), do: :pass + + def can?(user, %Ecto.Changeset{} = cs, action), + do: can?(user, apply_changes(cs), action) + + def can?(_, _, _), do: {:error, :forbidden} +end diff --git a/apps/core/lib/core/schema/oidc_provider.ex b/apps/core/lib/core/schema/oidc_provider.ex index c8af51c70..eb410642f 100644 --- a/apps/core/lib/core/schema/oidc_provider.ex +++ b/apps/core/lib/core/schema/oidc_provider.ex @@ -1,10 +1,12 @@ defmodule Core.Schema.OIDCProvider do use Piazza.Ecto.Schema - alias Core.Schema.{Installation, OIDCProviderBinding, Invite} + alias Core.Schema.{Installation, OIDCProviderBinding, Invite, User} defenum AuthMethod, post: 0, basic: 1 schema "oidc_providers" do + field :name, :string + field :description, :string field :client_id, :string field :client_secret, :string field :redirect_uris, {:array, :string} @@ -14,6 +16,7 @@ defmodule Core.Schema.OIDCProvider do field :login, :map, virtual: true belongs_to :installation, Installation + belongs_to :owner, User has_many :invites, Invite, foreign_key: :oidc_provider_id has_many :bindings, OIDCProviderBinding, @@ -23,7 +26,15 @@ defmodule Core.Schema.OIDCProvider do timestamps() end - @valid ~w(client_id client_secret installation_id redirect_uris auth_method)a + def for_owner(query \\ __MODULE__, owner_id) do + from(p in query, where: p.owner_id == ^owner_id) + end + + def ordered(query \\ __MODULE__, order \\ [asc: :name]) do + from(p in query, order_by: ^order) + end + + @valid ~w(name description client_id client_secret owner_id installation_id redirect_uris auth_method)a def changeset(model, attrs \\ %{}) do model @@ -32,5 +43,6 @@ defmodule Core.Schema.OIDCProvider do |> unique_constraint(:installation_id) |> unique_constraint(:client_id) |> foreign_key_constraint(:installation_id) + |> foreign_key_constraint(:owner_id) end end diff --git a/apps/core/lib/core/services/base.ex b/apps/core/lib/core/services/base.ex index 1826a74b4..c513827e3 100644 --- a/apps/core/lib/core/services/base.ex +++ b/apps/core/lib/core/services/base.ex @@ -1,13 +1,25 @@ defmodule Core.Services.Base do + alias Core.Schema.User + defmacro __using__(_) do quote do import Core.Services.Base + alias Core.Repo defp conf(key), do: Application.get_env(:core, __MODULE__)[key] end end + def find_bindings(%User{service_account: true} = user) do + case Core.Repo.preload(user, impersonation_policy: :bindings) do + %{impersonation_policy: %{bindings: [_ | _] = bindings}} -> + Enum.map(bindings, &Map.take(&1, [:group_id, :user_id])) + _ -> [] + end + end + def find_bindings(_), do: [] + def ok(val), do: {:ok, val} def error(val), do: {:error, val} diff --git a/apps/core/lib/core/services/oauth.ex b/apps/core/lib/core/services/oauth.ex index 3a4c65490..29ac67fe9 100644 --- a/apps/core/lib/core/services/oauth.ex +++ b/apps/core/lib/core/services/oauth.ex @@ -1,5 +1,7 @@ defmodule Core.Services.OAuth do use Core.Services.Base + import Core.Policies.OAuth + alias Core.PubSub alias Core.Schema.{User, OIDCProvider, OIDCLogin} alias Core.Clients.Hydra alias Core.Services.{Repositories, Audits} @@ -7,6 +9,90 @@ defmodule Core.Services.OAuth do @type error :: {:error, term} @type oauth_resp :: {:ok, %Hydra.Response{}} | error + @type oidc_resp :: {:ok, OidcProvider.t} | error + + @oidc_scopes "profile code openid offline_access offline" + @grant_types ~w(authorization_code refresh_token client_credentials) + + def get_provider(id), do: Repo.get(OIDCProvider, id) + + def get_provider!(id), do: Repo.get!(OIDCProvider, id) + + @doc """ + Creates a new oidc provider for a given installation, enabling a log-in with plural experience + """ + @spec create_oidc_provider(map, User.t) :: oidc_resp + def create_oidc_provider(attrs, %User{id: id} = user) do + start_transaction() + |> add_operation(:client, fn _ -> + Map.take(attrs, [:redirect_uris]) + |> Map.put(:scope, @oidc_scopes) + |> Map.put(:grant_types, @grant_types) + |> Map.put(:token_endpoint_auth_method, oidc_auth_method(attrs.auth_method)) + |> Hydra.create_client() + end) + |> add_operation(:oidc_provider, fn + %{client: %{client_id: cid, client_secret: secret}} -> + attrs = Map.merge(attrs, %{client_id: cid, client_secret: secret}) + |> add_bindings(find_bindings(user)) + %OIDCProvider{owner_id: id} + |> OIDCProvider.changeset(attrs) + |> allow(user, :create) + |> when_ok(:insert) + end) + |> execute(extract: :oidc_provider) + |> notify(:create) + end + + defp add_bindings(attrs, bindings) do + bindings = Enum.uniq_by((attrs[:bindings] || []) ++ bindings, & {&1[:group_id], &1[:user_id]}) + Map.put(attrs, :bindings, bindings) + end + + defp oidc_auth_method(:basic), do: "client_secret_basic" + defp oidc_auth_method(:post), do: "client_secret_post" + + @doc """ + Updates the spec of an installation's oidc provider + """ + @spec update_oidc_provider(map, binary, User.t) :: oidc_resp + def update_oidc_provider(attrs, id, %User{} = user) do + start_transaction() + |> add_operation(:oidc, fn _ -> + get_provider!(id) + |> Repo.preload([:bindings]) + |> OIDCProvider.changeset(attrs) + |> allow(user, :edit) + |> when_ok(:update) + end) + |> add_operation(:client, fn + %{oidc: %{client_id: id, auth_method: auth_method}} -> + attrs = Map.take(attrs, [:redirect_uris]) + |> Map.put(:scope, @oidc_scopes) + |> Map.put(:token_endpoint_auth_method, oidc_auth_method(auth_method)) + Hydra.update_client(id, attrs) + end) + |> execute(extract: :oidc) + |> notify(:update) + end + + @doc """ + Deletes an oidc provider and its hydra counterpart + """ + @spec delete_oidc_provider(binary, User.t) :: oidc_resp + def delete_oidc_provider(id, %User{} = user) do + start_transaction() + |> add_operation(:oidc, fn _ -> + get_provider!(id) + |> allow(user, :edit) + |> when_ok(:delete) + end) + |> add_operation(:client, fn %{oidc: %{client_id: id}} -> + with :ok <- Hydra.delete_client(id), + do: {:ok, nil} + end) + |> execute(extract: :oidc) + end @doc """ Gets the data related to a specific login @@ -85,4 +171,11 @@ defmodule Core.Services.OAuth do {:error, :failure} end end + + defp notify({:ok, %OIDCProvider{} = oidc}, :create), + do: handle_notify(PubSub.OIDCProviderCreated, oidc) + defp notify({:ok, %OIDCProvider{} = oidc}, :update), + do: handle_notify(PubSub.OIDCProviderUpdated, oidc) + + defp notify(pass, _), do: pass end diff --git a/apps/core/lib/core/services/repositories.ex b/apps/core/lib/core/services/repositories.ex index da86abe95..7ea29cb07 100644 --- a/apps/core/lib/core/services/repositories.ex +++ b/apps/core/lib/core/services/repositories.ex @@ -575,15 +575,6 @@ defmodule Core.Services.Repositories do Map.put(attrs, :bindings, bindings) end - defp find_bindings(%User{service_account: true} = user) do - case Core.Repo.preload(user, impersonation_policy: :bindings) do - %{impersonation_policy: %{bindings: [_ | _] = bindings}} -> - Enum.map(bindings, &Map.take(&1, [:group_id, :user_id])) - _ -> [] - end - end - defp find_bindings(_), do: [] - @doc """ Inserts or updates the oidc provider for an installation """ diff --git a/apps/core/priv/repo/migrations/20241005171529_add_owner_id_oidc_provider.exs b/apps/core/priv/repo/migrations/20241005171529_add_owner_id_oidc_provider.exs new file mode 100644 index 000000000..d1306fe36 --- /dev/null +++ b/apps/core/priv/repo/migrations/20241005171529_add_owner_id_oidc_provider.exs @@ -0,0 +1,11 @@ +defmodule Core.Repo.Migrations.AddOwnerIdOidcProvider do + use Ecto.Migration + + def change do + alter table(:oidc_providers) do + add :name, :string + add :description, :string + add :owner_id, references(:users, type: :uuid, on_delete: :delete_all) + end + end +end diff --git a/apps/core/test/services/oauth_test.exs b/apps/core/test/services/oauth_test.exs index 334175a99..a09e9c940 100644 --- a/apps/core/test/services/oauth_test.exs +++ b/apps/core/test/services/oauth_test.exs @@ -4,6 +4,81 @@ defmodule Core.Services.OAuthTest do alias Core.Schema.OIDCLogin use Mimic + describe "#create_oidc_provider/2" do + test "a user can create an oidc provider" do + account = insert(:account) + group = insert(:group, account: account) + expect(HTTPoison, :post, fn _, _, _ -> + {:ok, %{status_code: 200, body: Jason.encode!(%{client_id: "123", client_secret: "secret"})}} + end) + user = insert(:user) + + {:ok, oidc} = OAuth.create_oidc_provider(%{ + redirect_uris: ["https://example.com"], + auth_method: :basic, + bindings: [%{user_id: user.id}, %{group_id: group.id}] + }, user) + + assert oidc.client_id == "123" + assert oidc.client_secret == "secret" + assert oidc.redirect_uris == ["https://example.com"] + assert oidc.owner_id == user.id + + [first, second] = oidc.bindings + + assert first.user_id == user.id + assert second.group_id == group.id + end + end + + describe "#update_oidc_provider/2" do + test "you can update your own providers" do + user = insert(:user) + oidc = insert(:oidc_provider, owner: user) + expect(HTTPoison, :put, fn _, _, _ -> + {:ok, %{status_code: 200, body: Jason.encode!(%{client_id: "123", client_secret: "secret"})}} + end) + + {:ok, updated} = OAuth.update_oidc_provider(%{ + redirect_uris: ["https://example.com"], + auth_method: :basic + }, oidc.id, user) + + assert updated.id == oidc.id + assert updated.auth_method == :basic + end + + test "others cannot update your provider" do + user = insert(:user) + oidc = insert(:oidc_provider, owner: user) + + {:error, :forbidden} = OAuth.update_oidc_provider(%{ + redirect_uris: ["https://example.com"], + auth_method: :basic + }, oidc.id, insert(:user)) + end + end + + describe "#delete_oidc_provider/2" do + test "you can delete your own providers" do + user = insert(:user) + oidc = insert(:oidc_provider, owner: user) + expect(HTTPoison, :delete, fn _, _ -> {:ok, %{status_code: 204, body: ""}} end) + + {:ok, deleted} = OAuth.delete_oidc_provider(oidc.id, user) + + assert deleted.id == oidc.id + refute refetch(deleted) + end + + test "others cannot delete your provider" do + user = insert(:user) + oidc = insert(:oidc_provider, owner: user) + + {:error, :forbidden} = OAuth.delete_oidc_provider(oidc.id, insert(:user)) + end + end + describe "#get_login/1" do test "It can get information related to an oauth login" do provider = insert(:oidc_provider) diff --git a/apps/graphql/lib/graphql/resolvers/oauth.ex b/apps/graphql/lib/graphql/resolvers/oauth.ex index 148cd0264..025d761fa 100644 --- a/apps/graphql/lib/graphql/resolvers/oauth.ex +++ b/apps/graphql/lib/graphql/resolvers/oauth.ex @@ -1,6 +1,6 @@ defmodule GraphQl.Resolvers.OAuth do use GraphQl.Resolvers.Base, model: Core.Schema.OIDCProvider - alias Core.Schema.OIDCLogin + alias Core.Schema.{OIDCLogin, OIDCProvider} alias Core.Services.{OAuth, Users} alias Core.OAuth, as: OAuthHandler alias GraphQl.Resolvers.User @@ -11,6 +11,12 @@ defmodule GraphQl.Resolvers.OAuth do |> paginate(args) end + def list_oidc_providers(args, %{context: %{current_user: user}}) do + OIDCProvider.for_owner(user.id) + |> OIDCProvider.ordered() + |> paginate(args) + end + def login_metrics(_, %{context: %{current_user: user}}) do cutoff = Timex.now() |> Timex.shift(months: -1) OIDCLogin.for_account(user.account_id) diff --git a/apps/graphql/lib/graphql/resolvers/repository.ex b/apps/graphql/lib/graphql/resolvers/repository.ex index f6ab919f2..bfb20be11 100644 --- a/apps/graphql/lib/graphql/resolvers/repository.ex +++ b/apps/graphql/lib/graphql/resolvers/repository.ex @@ -1,6 +1,6 @@ defmodule GraphQl.Resolvers.Repository do use GraphQl.Resolvers.Base, model: Core.Schema.Repository - alias Core.Services.{Repositories, Users} + alias Core.Services.{Repositories, Users, OAuth} alias Core.Schema.{ Installation, Integration, @@ -205,15 +205,23 @@ defmodule GraphQl.Resolvers.Repository do def reset_installations(_, %{context: %{current_user: user}}), do: Repositories.reset_installations(user) - def create_oidc_provider(%{attributes: attrs, installation_id: id}, %{context: %{current_user: user}}), - do: Repositories.create_oidc_provider(attrs, id, user) + def create_oidc_provider(%{attributes: attrs, installation_id: id}, %{context: %{current_user: user}}) + when is_binary(id), do: Repositories.create_oidc_provider(attrs, id, user) + def create_oidc_provider(%{attributes: attrs}, %{context: %{current_user: user}}), + do: OAuth.create_oidc_provider(attrs, user) - def update_oidc_provider(%{attributes: attrs, installation_id: id}, %{context: %{current_user: user}}), - do: Repositories.update_oidc_provider(attrs, id, user) + def update_oidc_provider(%{attributes: attrs, installation_id: id}, %{context: %{current_user: user}}) + when is_binary(id), do: Repositories.update_oidc_provider(attrs, id, user) + def update_oidc_provider(%{attributes: attrs, id: id}, %{context: %{current_user: user}}) + when is_binary(id), do: OAuth.update_oidc_provider(attrs, id, user) + def update_oidc_provider(_, _), do: {:error, "you must provide either id or installation id"} def upsert_oidc_provider(%{attributes: attrs, installation_id: id}, %{context: %{current_user: user}}), do: Repositories.upsert_oidc_provider(attrs, id, user) + def delete_oidc_provider(%{id: id}, %{context: %{current_user: user}}) + when is_binary(id), do: OAuth.delete_oidc_provider(id, user) + def create_artifact(%{repository_id: repo_id, attributes: attrs}, %{context: %{current_user: user}}), do: Repositories.create_artifact(attrs, repo_id, user) def create_artifact(%{repository_name: name, attributes: attrs}, %{context: %{current_user: user}}) do diff --git a/apps/graphql/lib/graphql/schema/oauth.ex b/apps/graphql/lib/graphql/schema/oauth.ex index 90e2c79eb..199f581a7 100644 --- a/apps/graphql/lib/graphql/schema/oauth.ex +++ b/apps/graphql/lib/graphql/schema/oauth.ex @@ -100,6 +100,12 @@ defmodule GraphQl.Schema.OAuth do resolve &OAuth.login_metrics/2 end + + connection field :oidc_providers, node_type: :oidc_provider do + middleware Authenticated + + safe_resolve &OAuth.list_oidc_providers/2 + end end object :oauth_mutations do diff --git a/apps/graphql/lib/graphql/schema/repository.ex b/apps/graphql/lib/graphql/schema/repository.ex index 2897fee0b..420f45590 100644 --- a/apps/graphql/lib/graphql/schema/repository.ex +++ b/apps/graphql/lib/graphql/schema/repository.ex @@ -96,9 +96,11 @@ defmodule GraphQl.Schema.Repository do @desc "Input for creating or updating the OIDC attributes of an application installation." input_object :oidc_attributes do + field :name, :string + field :description, :string field :redirect_uris, list_of(:string), description: "The redirect URIs for the OIDC provider." - field :auth_method, non_null(:oidc_auth_method), description: "The authentication method for the OIDC provider." - field :bindings, list_of(:binding_attributes), description: "The users or groups that can login through the OIDC provider." + field :auth_method, non_null(:oidc_auth_method), description: "The authentication method for the OIDC provider." + field :bindings, list_of(:binding_attributes), description: "The users or groups that can login through the OIDC provider." end input_object :lock_attributes do @@ -284,6 +286,8 @@ defmodule GraphQl.Schema.Repository do object :oidc_provider do field :id, non_null(:id) + field :name, :string + field :description, :string field :client_secret, non_null(:string) field :client_id, non_null(:string) field :redirect_uris, list_of(:string) @@ -292,6 +296,7 @@ defmodule GraphQl.Schema.Repository do field :consent, :consent_request + field :owner, :user, resolve: dataloader(User) field :invites, list_of(:invite), resolve: dataloader(Account) field :bindings, list_of(:oidc_provider_binding), resolve: dataloader(Repository) @@ -329,6 +334,7 @@ defmodule GraphQl.Schema.Repository do connection node_type: :repository connection node_type: :installation connection node_type: :integration + connection node_type: :oidc_provider object :repository_queries do @desc "Get an application by its ID or name." @@ -496,16 +502,17 @@ defmodule GraphQl.Schema.Repository do field :create_oidc_provider, :oidc_provider do middleware Authenticated, :external - arg :installation_id, non_null(:id), description: "The installation ID" - arg :attributes, non_null(:oidc_attributes) + arg :installation_id, :id, description: "The installation ID this provider will be bound to" + arg :attributes, non_null(:oidc_attributes) safe_resolve &Repository.create_oidc_provider/2 end field :update_oidc_provider, :oidc_provider do middleware Authenticated, :external - arg :installation_id, non_null(:id) - arg :attributes, non_null(:oidc_attributes) + arg :id, :id + arg :installation_id, :id + arg :attributes, non_null(:oidc_attributes) safe_resolve &Repository.update_oidc_provider/2 end @@ -513,11 +520,18 @@ defmodule GraphQl.Schema.Repository do field :upsert_oidc_provider, :oidc_provider do middleware Authenticated, :external arg :installation_id, non_null(:id) - arg :attributes, non_null(:oidc_attributes) + arg :attributes, non_null(:oidc_attributes) safe_resolve &Repository.upsert_oidc_provider/2 end + field :delete_oidc_provider, :oidc_provider do + middleware Authenticated + arg :id, non_null(:id) + + safe_resolve &Repository.delete_oidc_provider/2 + end + field :acquire_lock, :apply_lock do middleware Authenticated arg :repository, non_null(:string) diff --git a/apps/graphql/test/mutations/repository_mutation_test.exs b/apps/graphql/test/mutations/repository_mutation_test.exs index 408466d30..d7f8759a0 100644 --- a/apps/graphql/test/mutations/repository_mutation_test.exs +++ b/apps/graphql/test/mutations/repository_mutation_test.exs @@ -340,6 +340,106 @@ defmodule GraphQl.RepositoryMutationsTest do [%{"group" => g}] = provider["bindings"] assert g["id"] == group.id end + + test "it will create an user-bound oidc provider" do + user = insert(:user) + account = insert(:account) + group = insert(:group, account: account) + expect(HTTPoison, :post, fn _, _, _ -> + {:ok, %{status_code: 200, body: Jason.encode!(%{client_id: "123", client_secret: "secret"})}} + end) + + expect(HTTPoison, :get, fn _, _ -> + {:ok, %{status_code: 200, body: Jason.encode!(%{issuer: "https://oidc.plural.sh/"})}} + end) + + {:ok, %{data: %{"createOidcProvider" => provider}}} = run_query(""" + mutation Create($attributes: OidcAttributes!) { + createOidcProvider(attributes: $attributes) { + id + clientId + clientSecret + redirectUris + authMethod + bindings { + user { id } + group { id } + } + configuration { + issuer + } + } + } + """, %{ + "attributes" => %{ + "authMethod" => "BASIC", + "redirectUris" => ["example.com"], + "bindings" => [%{"groupId" => group.id}] + } + }, %{current_user: user}) + + assert provider["id"] + assert provider["clientId"] == "123" + assert provider["authMethod"] == "BASIC" + assert provider["clientSecret"] == "secret" + assert provider["redirectUris"] == ["example.com"] + assert provider["configuration"]["issuer"] == "https://oidc.plural.sh/" + + [%{"group" => g}] = provider["bindings"] + assert g["id"] == group.id + end + end + + describe "updateOidcProvider" do + test "it can update a user-bound oidc provider" do + user = insert(:user) + oidc = insert(:oidc_provider, owner: user) + expect(HTTPoison, :put, fn _, _, _ -> + {:ok, %{status_code: 200, body: Jason.encode!(%{client_id: "123", client_secret: "secret"})}} + end) + + {:ok, %{data: %{"updateOidcProvider" => updated}}} = run_query(""" + mutation Update($id: ID!, $attrs: OidcAttributes!) { + updateOidcProvider(id: $id, attributes: $attrs) { + id + redirectUris + authMethod + } + } + """, %{ + "id" => oidc.id, + "attrs" => %{ + "authMethod" => "BASIC", + "redirectUris" => ["example.com"], + } + }, %{current_user: user}) + + assert updated["id"] == oidc.id + assert updated["authMethod"] == "BASIC" + assert updated["redirectUris"] == ["example.com"] + end + end + + describe "deleteOidcProvider" do + test "it can update a user-bound oidc provider" do + user = insert(:user) + oidc = insert(:oidc_provider, owner: user) + expect(HTTPoison, :delete, fn _, _ -> {:ok, %{status_code: 204, body: ""}} end) + + {:ok, %{data: %{"deleteOidcProvider" => updated}}} = run_query(""" + mutation Update($id: ID!) { + deleteOidcProvider(id: $id) { + id + clientId + clientSecret + redirectUris + authMethod + } + } + """, %{"id" => oidc.id}, %{current_user: user}) + + assert updated["id"] == oidc.id + end end describe "updateDockerRepository" do diff --git a/apps/graphql/test/queries/oauth_queries_test.exs b/apps/graphql/test/queries/oauth_queries_test.exs index 1678d2260..2f1f8f657 100644 --- a/apps/graphql/test/queries/oauth_queries_test.exs +++ b/apps/graphql/test/queries/oauth_queries_test.exs @@ -3,6 +3,27 @@ defmodule GraphQl.OAuthQueriesTest do import GraphQl.TestHelpers use Mimic + describe "oidcProviders" do + test "it will list oidc providers" do + user = insert(:user) + providers = insert_list(3, :oidc_provider, owner: user) + insert_list(2, :oidc_provider) + + {:ok, %{data: %{"oidcProviders" => found}}} = run_query(""" + query { + oidcProviders(first: 5) { + edges { + node { id } + } + } + } + """, %{}, %{current_user: user}) + + assert from_connection(found) + |> ids_equal(providers) + end + end + describe "oauthLogin" do test "it can fetch an oauth login's details" do provider = insert(:oidc_provider) diff --git a/config/config.exs b/config/config.exs index d8dc3b7a2..d294fe583 100644 --- a/config/config.exs +++ b/config/config.exs @@ -197,4 +197,6 @@ config :recaptcha, public_key: {:system, "RECAPTCHA_SITE_KEY"}, secret: {:system, "RECAPTURE_SECRET_KEY"} +config :tzdata, :autoupdate, :disabled + import_config "#{config_env()}.exs" diff --git a/schema/schema.graphql b/schema/schema.graphql index eee8c72ac..fa2a2b877 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -166,6 +166,8 @@ type RootQueryType { loginMetrics: [GeoMetric] + oidcProviders(after: String, first: Int, before: String, last: Int): OidcProviderConnection + dnsDomain(id: ID!): DnsDomain dnsDomains(after: String, first: Int, before: String, last: Int, q: String): DnsDomainConnection @@ -324,16 +326,18 @@ type RootMutationType { createArtifact(repositoryId: ID, repositoryName: String, attributes: ArtifactAttributes!): Artifact createOidcProvider( - "The installation ID" - installationId: ID! + "The installation ID this provider will be bound to" + installationId: ID attributes: OidcAttributes! ): OidcProvider - updateOidcProvider(installationId: ID!, attributes: OidcAttributes!): OidcProvider + updateOidcProvider(id: ID, installationId: ID, attributes: OidcAttributes!): OidcProvider upsertOidcProvider(installationId: ID!, attributes: OidcAttributes!): OidcProvider + deleteOidcProvider(id: ID!): OidcProvider + acquireLock(repository: String!): ApplyLock releaseLock(repository: String!, attributes: LockAttributes!): ApplyLock @@ -2642,6 +2646,10 @@ input ArtifactAttributes { "Input for creating or updating the OIDC attributes of an application installation." input OidcAttributes { + name: String + + description: String + "The redirect URIs for the OIDC provider." redirectUris: [String] @@ -2900,12 +2908,15 @@ type Integration { type OidcProvider { id: ID! + name: String + description: String clientSecret: String! clientId: String! redirectUris: [String] authMethod: OidcAuthMethod! configuration: OuathConfiguration consent: ConsentRequest + owner: User invites: [Invite] bindings: [OidcProviderBinding] insertedAt: DateTime @@ -2954,6 +2965,11 @@ type IntegrationConnection { edges: [IntegrationEdge] } +type OidcProviderConnection { + pageInfo: PageInfo! + edges: [OidcProviderEdge] +} + enum PlanType { LICENSED METERED @@ -3755,6 +3771,11 @@ type InvoiceEdge { cursor: String } +type OidcProviderEdge { + node: OidcProvider + cursor: String +} + type IntegrationEdge { node: Integration cursor: String diff --git a/www/src/generated/graphql.ts b/www/src/generated/graphql.ts index c8864237e..0e72bcecb 100644 --- a/www/src/generated/graphql.ts +++ b/www/src/generated/graphql.ts @@ -1821,6 +1821,8 @@ export type OidcAttributes = { authMethod: OidcAuthMethod; /** The users or groups that can login through the OIDC provider. */ bindings?: InputMaybe>>; + description?: InputMaybe; + name?: InputMaybe; /** The redirect URIs for the OIDC provider. */ redirectUris?: InputMaybe>>; }; @@ -1866,9 +1868,12 @@ export type OidcProvider = { clientSecret: Scalars['String']['output']; configuration?: Maybe; consent?: Maybe; + description?: Maybe; id: Scalars['ID']['output']; insertedAt?: Maybe; invites?: Maybe>>; + name?: Maybe; + owner?: Maybe; redirectUris?: Maybe>>; updatedAt?: Maybe; }; @@ -1882,6 +1887,18 @@ export type OidcProviderBinding = { user?: Maybe; }; +export type OidcProviderConnection = { + __typename?: 'OidcProviderConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type OidcProviderEdge = { + __typename?: 'OidcProviderEdge'; + cursor?: Maybe; + node?: Maybe; +}; + export type OidcSettings = { __typename?: 'OidcSettings'; authMethod: OidcAuthMethod; @@ -2864,6 +2881,7 @@ export type RootMutationType = { deleteInvite?: Maybe; deleteKeyBackup?: Maybe; deleteMessage?: Maybe; + deleteOidcProvider?: Maybe; deletePaymentMethod?: Maybe; deletePlatformSubscription?: Maybe; deletePublicKey?: Maybe; @@ -3080,7 +3098,7 @@ export type RootMutationTypeCreateOauthIntegrationArgs = { export type RootMutationTypeCreateOidcProviderArgs = { attributes: OidcAttributes; - installationId: Scalars['ID']['input']; + installationId?: InputMaybe; }; @@ -3296,6 +3314,11 @@ export type RootMutationTypeDeleteMessageArgs = { }; +export type RootMutationTypeDeleteOidcProviderArgs = { + id: Scalars['ID']['input']; +}; + + export type RootMutationTypeDeletePaymentMethodArgs = { id: Scalars['ID']['input']; }; @@ -3632,7 +3655,8 @@ export type RootMutationTypeUpdateMessageArgs = { export type RootMutationTypeUpdateOidcProviderArgs = { attributes: OidcAttributes; - installationId: Scalars['ID']['input']; + id?: InputMaybe; + installationId?: InputMaybe; }; @@ -3793,6 +3817,7 @@ export type RootQueryType = { oidcConsent?: Maybe; oidcLogin?: Maybe; oidcLogins?: Maybe; + oidcProviders?: Maybe; oidcToken?: Maybe; platformMetrics?: Maybe; platformPlans?: Maybe>>; @@ -4148,6 +4173,14 @@ export type RootQueryTypeOidcLoginsArgs = { }; +export type RootQueryTypeOidcProvidersArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + export type RootQueryTypeOidcTokenArgs = { email: Scalars['String']['input']; idToken: Scalars['String']['input'];