diff --git a/app/graphql/types/application_type.rb b/app/graphql/types/application_type.rb index a79c78bfa..f317387a3 100644 --- a/app/graphql/types/application_type.rb +++ b/app/graphql/types/application_type.rb @@ -22,6 +22,17 @@ class ApplicationType < Types::BaseObject null: true, description: 'URL to the legal notice page' + # rubocop:disable GraphQL/ExtractType -- one is the list and the second one is for on-demand querying + field :identity_providers, Types::IdentityProviderBasicType.connection_type, + null: false, + description: 'Configured identity providers for login and registration' + + field :identity_provider_login_url, String, null: true, + description: 'Login URL for a specific identity provider' do + argument :id, String, required: true, description: 'ID of the identity provider' + end + # rubocop:enable GraphQL/ExtractType + expose_abilities %i[ create_organization create_runtime @@ -50,6 +61,17 @@ def terms_and_conditions_url def legal_notice_url ApplicationSetting.current[:legal_notice_url] end + + def identity_providers + ApplicationSetting.current[:identity_providers] + end + + def identity_provider_login_url(id:) + provider = Users::Identity::BaseService.new.identity_provider.providers[id] + return nil if provider.nil? + + provider.authorization_url + end end end diff --git a/app/graphql/types/identity_provider_basic_type.rb b/app/graphql/types/identity_provider_basic_type.rb new file mode 100644 index 000000000..1872cf8fd --- /dev/null +++ b/app/graphql/types/identity_provider_basic_type.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + class IdentityProviderBasicType < Types::BaseObject + description 'Represents a basic identity provider.' + + field :id, String, null: false, description: 'Unique identifier of the identity provider.' + field :type, Types::IdentityProviderTypeEnum, null: false, description: 'Type of the identity provider.' + end +end diff --git a/app/graphql/types/identity_provider_type.rb b/app/graphql/types/identity_provider_type.rb index f223abe34..9e5a74a16 100644 --- a/app/graphql/types/identity_provider_type.rb +++ b/app/graphql/types/identity_provider_type.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true module Types - class IdentityProviderType < Types::BaseObject + class IdentityProviderType < IdentityProviderBasicType description 'Represents an identity provider configuration.' - field :id, String, null: false, description: 'Unique identifier of the identity provider.' - field :type, Types::IdentityProviderTypeEnum, null: false, description: 'Type of the identity provider.' - field :config, Types::IdentityProviderConfigType, null: false, description: 'Configuration details of the identity provider.' diff --git a/docs/graphql/object/application.md b/docs/graphql/object/application.md index 0985557b3..de547787a 100644 --- a/docs/graphql/object/application.md +++ b/docs/graphql/object/application.md @@ -9,6 +9,7 @@ Represents the application instance | Name | Type | Description | |------|------|-------------| | `currentLicense` | [`License`](../object/license.md) | (EE only) Currently active license of the instance | +| `identityProviders` | [`IdentityProviderBasicConnection!`](../object/identityproviderbasicconnection.md) | Configured identity providers for login and registration | | `legalNoticeUrl` | [`String`](../scalar/string.md) | URL to the legal notice page | | `licenses` | [`LicenseConnection!`](../object/licenseconnection.md) | (EE only) Licenses of the instance | | `metadata` | [`Metadata`](../object/metadata.md) | Metadata about the application | @@ -16,3 +17,15 @@ Represents the application instance | `settings` | [`ApplicationSettings`](../object/applicationsettings.md) | Global application settings | | `termsAndConditionsUrl` | [`String`](../scalar/string.md) | URL to the terms and conditions page | | `userAbilities` | [`ApplicationUserAbilities!`](../object/applicationuserabilities.md) | Abilities for the current user on this Application | + +## Fields with arguments + +### identityProviderLoginUrl + +Login URL for a specific identity provider + +Returns [`String`](../scalar/string.md). + +| Name | Type | Description | +|------|------|-------------| +| `id` | [`String!`](../scalar/string.md) | ID of the identity provider | diff --git a/docs/graphql/object/identityproviderbasic.md b/docs/graphql/object/identityproviderbasic.md new file mode 100644 index 000000000..a8b8449b1 --- /dev/null +++ b/docs/graphql/object/identityproviderbasic.md @@ -0,0 +1,12 @@ +--- +title: IdentityProviderBasic +--- + +Represents a basic identity provider. + +## Fields without arguments + +| Name | Type | Description | +|------|------|-------------| +| `id` | [`String!`](../scalar/string.md) | Unique identifier of the identity provider. | +| `type` | [`IdentityProviderType!`](../enum/identityprovidertype.md) | Type of the identity provider. | diff --git a/docs/graphql/object/identityproviderbasicconnection.md b/docs/graphql/object/identityproviderbasicconnection.md new file mode 100644 index 000000000..8bb3b06ed --- /dev/null +++ b/docs/graphql/object/identityproviderbasicconnection.md @@ -0,0 +1,14 @@ +--- +title: IdentityProviderBasicConnection +--- + +The connection type for IdentityProviderBasic. + +## Fields without arguments + +| Name | Type | Description | +|------|------|-------------| +| `count` | [`Int!`](../scalar/int.md) | Total count of collection. | +| `edges` | [`[IdentityProviderBasicEdge]`](../object/identityproviderbasicedge.md) | A list of edges. | +| `nodes` | [`[IdentityProviderBasic]`](../object/identityproviderbasic.md) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](../object/pageinfo.md) | Information to aid in pagination. | diff --git a/docs/graphql/object/identityproviderbasicedge.md b/docs/graphql/object/identityproviderbasicedge.md new file mode 100644 index 000000000..ae799b83d --- /dev/null +++ b/docs/graphql/object/identityproviderbasicedge.md @@ -0,0 +1,12 @@ +--- +title: IdentityProviderBasicEdge +--- + +An edge in a connection. + +## Fields without arguments + +| Name | Type | Description | +|------|------|-------------| +| `cursor` | [`String!`](../scalar/string.md) | A cursor for use in pagination. | +| `node` | [`IdentityProviderBasic`](../object/identityproviderbasic.md) | The item at the end of the edge. | diff --git a/extensions/ee/spec/graphql/ee/types/application_type_spec.rb b/extensions/ee/spec/graphql/ee/types/application_type_spec.rb index d48bcaba4..2b83ee735 100644 --- a/extensions/ee/spec/graphql/ee/types/application_type_spec.rb +++ b/extensions/ee/spec/graphql/ee/types/application_type_spec.rb @@ -12,6 +12,8 @@ legalNoticeUrl licenses currentLicense + identityProviders + identityProviderLoginUrl user_abilities ] end diff --git a/spec/graphql/types/application_type_spec.rb b/spec/graphql/types/application_type_spec.rb index 02a1af04f..ed919c461 100644 --- a/spec/graphql/types/application_type_spec.rb +++ b/spec/graphql/types/application_type_spec.rb @@ -10,6 +10,8 @@ privacyUrl termsAndConditionsUrl legalNoticeUrl + identityProviders + identityProviderLoginUrl user_abilities ] end diff --git a/spec/requests/graphql/query/identity_provider_login_url_query_spec.rb b/spec/requests/graphql/query/identity_provider_login_url_query_spec.rb new file mode 100644 index 000000000..e7fce826b --- /dev/null +++ b/spec/requests/graphql/query/identity_provider_login_url_query_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'application identityProviderLoginUrl Query' do + include GraphqlHelpers + + let(:query) do + <<~QUERY + query($id: String!) { + application { + identityProviderLoginUrl(id: $id) + } + } + QUERY + end + + let(:current_user) { nil } + let(:variables) { { id: provider_id } } + + before do + stub_application_settings(identity_providers: identity_providers) + post_graphql(query, variables: variables, current_user: current_user) + end + + context 'with an OIDC provider' do + let(:provider_id) { 'my-oidc' } + + let(:identity_providers) do + [ + { + id: 'my-oidc', + type: 'oidc', + config: { + client_id: 'oidc-client-id', + client_secret: 'oidc-client-secret', + redirect_uri: 'https://example.com/callback', + user_details_url: 'https://idp.example.com/userinfo', + authorization_url: 'https://idp.example.com/authorize?client_id={client_id}&redirect_uri={redirect_uri}', + }, + } + ] + end + + it 'returns the login url with substituted parameters' do + login_url = graphql_data_at(:application, :identity_provider_login_url) + + expect(login_url).to eq('https://idp.example.com/authorize?client_id=oidc-client-id&redirect_uri=https://example.com/callback') + end + end + + context 'with a SAML provider' do + let(:provider_id) { 'my-saml' } + + let(:identity_providers) do + [ + { + id: 'my-saml', + type: 'saml', + config: { + settings: { + idp_sso_service_url: 'https://idp.example.com/saml/sso', + idp_cert_fingerprint: 'AB:CD:EF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00', + sp_entity_id: 'https://example.com/saml/metadata', + }, + }, + } + ] + end + + it 'returns the login url' do + login_url = graphql_data_at(:application, :identity_provider_login_url) + + expect(login_url).to be_present + expect(login_url).to include('https://idp.example.com/saml/sso?SAMLRequest=') + end + end + + context 'when provider does not exist' do + let(:provider_id) { 'nonexistent' } + let(:identity_providers) { [] } + + it 'returns null' do + login_url = graphql_data_at(:application, :identity_provider_login_url) + + expect(login_url).to be_nil + end + end + + context 'when querying without authentication' do + let(:provider_id) { 'my-oidc' } + + let(:identity_providers) do + [ + { + id: 'my-oidc', + type: 'oidc', + config: { + client_id: 'oidc-client-id', + client_secret: 'oidc-client-secret', + redirect_uri: 'https://example.com/callback', + user_details_url: 'https://idp.example.com/userinfo', + authorization_url: 'https://idp.example.com/authorize?client_id={client_id}&redirect_uri={redirect_uri}', + }, + } + ] + end + + it 'returns the login url without requiring authentication' do + login_url = graphql_data_at(:application, :identity_provider_login_url) + + expect(login_url).to be_present + end + end +end