Add saml domain verification
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
5
pkg/coredata/migrations/20251219T220314Z.sql
Normal file
5
pkg/coredata/migrations/20251219T220314Z.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE auth_saml_configurations DROP COLUMN enabled;
|
||||||
|
ALTER TABLE auth_saml_configurations DROP COLUMN domain_verified;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_saml_config_domain_org_unique
|
||||||
|
ON auth_saml_configurations(organization_id, email_domain);
|
||||||
3
pkg/coredata/migrations/20251219T230719Z.sql
Normal file
3
pkg/coredata/migrations/20251219T230719Z.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
CREATE UNIQUE INDEX idx_saml_config_domain_verification_token_unique
|
||||||
|
ON auth_saml_configurations(domain_verification_token);
|
||||||
|
|
||||||
@@ -43,7 +43,6 @@ type (
|
|||||||
AttributeLastname string `db:"attribute_lastname"`
|
AttributeLastname string `db:"attribute_lastname"`
|
||||||
AttributeRole string `db:"attribute_role"`
|
AttributeRole string `db:"attribute_role"`
|
||||||
AutoSignupEnabled bool `db:"auto_signup_enabled"`
|
AutoSignupEnabled bool `db:"auto_signup_enabled"`
|
||||||
DomainVerified bool `db:"domain_verified"`
|
|
||||||
DomainVerificationToken *string `db:"domain_verification_token"`
|
DomainVerificationToken *string `db:"domain_verification_token"`
|
||||||
DomainVerifiedAt *time.Time `db:"domain_verified_at"`
|
DomainVerifiedAt *time.Time `db:"domain_verified_at"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
@@ -88,7 +87,6 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
organization_id,
|
organization_id,
|
||||||
email_domain,
|
email_domain,
|
||||||
enabled,
|
|
||||||
enforcement_policy,
|
enforcement_policy,
|
||||||
idp_entity_id,
|
idp_entity_id,
|
||||||
idp_sso_url,
|
idp_sso_url,
|
||||||
@@ -99,7 +97,6 @@ SELECT
|
|||||||
attribute_lastname,
|
attribute_lastname,
|
||||||
attribute_role,
|
attribute_role,
|
||||||
auto_signup_enabled,
|
auto_signup_enabled,
|
||||||
domain_verified,
|
|
||||||
domain_verification_token,
|
domain_verification_token,
|
||||||
domain_verified_at,
|
domain_verified_at,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -147,7 +144,6 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
organization_id,
|
organization_id,
|
||||||
email_domain,
|
email_domain,
|
||||||
enabled,
|
|
||||||
enforcement_policy,
|
enforcement_policy,
|
||||||
idp_entity_id,
|
idp_entity_id,
|
||||||
idp_sso_url,
|
idp_sso_url,
|
||||||
@@ -158,7 +154,6 @@ SELECT
|
|||||||
attribute_lastname,
|
attribute_lastname,
|
||||||
attribute_role,
|
attribute_role,
|
||||||
auto_signup_enabled,
|
auto_signup_enabled,
|
||||||
domain_verified,
|
|
||||||
domain_verification_token,
|
domain_verification_token,
|
||||||
domain_verified_at,
|
domain_verified_at,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -195,6 +190,58 @@ LIMIT 1;
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SAMLConfiguration) LoadByIDForUpdateSkipLocked(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
configID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
email_domain,
|
||||||
|
enforcement_policy,
|
||||||
|
idp_entity_id,
|
||||||
|
idp_sso_url,
|
||||||
|
idp_certificate,
|
||||||
|
idp_metadata_url,
|
||||||
|
attribute_email,
|
||||||
|
attribute_firstname,
|
||||||
|
attribute_lastname,
|
||||||
|
attribute_role,
|
||||||
|
auto_signup_enabled,
|
||||||
|
domain_verification_token,
|
||||||
|
domain_verified_at,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
auth_saml_configurations
|
||||||
|
WHERE
|
||||||
|
id = @id
|
||||||
|
FOR UPDATE SKIP LOCKED;
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"id": configID}
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query auth_saml_configurations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SAMLConfiguration])
|
||||||
|
if err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot collect saml_configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = config
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SAMLConfiguration) Insert(
|
func (s *SAMLConfiguration) Insert(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
@@ -216,7 +263,6 @@ INSERT INTO auth_saml_configurations (
|
|||||||
attribute_lastname,
|
attribute_lastname,
|
||||||
attribute_role,
|
attribute_role,
|
||||||
auto_signup_enabled,
|
auto_signup_enabled,
|
||||||
domain_verified,
|
|
||||||
domain_verification_token,
|
domain_verification_token,
|
||||||
domain_verified_at,
|
domain_verified_at,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -236,7 +282,6 @@ INSERT INTO auth_saml_configurations (
|
|||||||
@attribute_lastname,
|
@attribute_lastname,
|
||||||
@attribute_role,
|
@attribute_role,
|
||||||
@auto_signup_enabled,
|
@auto_signup_enabled,
|
||||||
@domain_verified,
|
|
||||||
@domain_verification_token,
|
@domain_verification_token,
|
||||||
@domain_verified_at,
|
@domain_verified_at,
|
||||||
@created_at,
|
@created_at,
|
||||||
@@ -259,7 +304,6 @@ INSERT INTO auth_saml_configurations (
|
|||||||
"attribute_lastname": s.AttributeLastname,
|
"attribute_lastname": s.AttributeLastname,
|
||||||
"attribute_role": s.AttributeRole,
|
"attribute_role": s.AttributeRole,
|
||||||
"auto_signup_enabled": s.AutoSignupEnabled,
|
"auto_signup_enabled": s.AutoSignupEnabled,
|
||||||
"domain_verified": s.DomainVerified,
|
|
||||||
"domain_verification_token": s.DomainVerificationToken,
|
"domain_verification_token": s.DomainVerificationToken,
|
||||||
"domain_verified_at": s.DomainVerifiedAt,
|
"domain_verified_at": s.DomainVerifiedAt,
|
||||||
"created_at": s.CreatedAt,
|
"created_at": s.CreatedAt,
|
||||||
@@ -292,7 +336,6 @@ SET
|
|||||||
attribute_lastname = @attribute_lastname,
|
attribute_lastname = @attribute_lastname,
|
||||||
attribute_role = @attribute_role,
|
attribute_role = @attribute_role,
|
||||||
auto_signup_enabled = @auto_signup_enabled,
|
auto_signup_enabled = @auto_signup_enabled,
|
||||||
domain_verified = @domain_verified,
|
|
||||||
domain_verification_token = @domain_verification_token,
|
domain_verification_token = @domain_verification_token,
|
||||||
domain_verified_at = @domain_verified_at,
|
domain_verified_at = @domain_verified_at,
|
||||||
updated_at = @updated_at
|
updated_at = @updated_at
|
||||||
@@ -315,7 +358,6 @@ WHERE
|
|||||||
"attribute_lastname": s.AttributeLastname,
|
"attribute_lastname": s.AttributeLastname,
|
||||||
"attribute_role": s.AttributeRole,
|
"attribute_role": s.AttributeRole,
|
||||||
"auto_signup_enabled": s.AutoSignupEnabled,
|
"auto_signup_enabled": s.AutoSignupEnabled,
|
||||||
"domain_verified": s.DomainVerified,
|
|
||||||
"domain_verification_token": s.DomainVerificationToken,
|
"domain_verification_token": s.DomainVerificationToken,
|
||||||
"domain_verified_at": s.DomainVerifiedAt,
|
"domain_verified_at": s.DomainVerifiedAt,
|
||||||
"updated_at": s.UpdatedAt,
|
"updated_at": s.UpdatedAt,
|
||||||
@@ -377,7 +419,6 @@ SELECT
|
|||||||
attribute_lastname,
|
attribute_lastname,
|
||||||
attribute_role,
|
attribute_role,
|
||||||
auto_signup_enabled,
|
auto_signup_enabled,
|
||||||
domain_verified,
|
|
||||||
domain_verification_token,
|
domain_verification_token,
|
||||||
domain_verified_at,
|
domain_verified_at,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -410,122 +451,6 @@ ORDER BY email_domain ASC;
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAllEnabledSAMLConfigurationsByEmailDomain loads all enabled SAML configurations for a given email domain
|
|
||||||
// This is used for SSO login detection when multiple organizations may have SAML configured for the same domain
|
|
||||||
func LoadAllEnabledSAMLConfigurationsByEmailDomain(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
emailDomain string,
|
|
||||||
) ([]*SAMLConfiguration, error) {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
email_domain,
|
|
||||||
enforcement_policy,
|
|
||||||
idp_entity_id,
|
|
||||||
idp_sso_url,
|
|
||||||
idp_certificate,
|
|
||||||
idp_metadata_url,
|
|
||||||
attribute_email,
|
|
||||||
attribute_firstname,
|
|
||||||
attribute_lastname,
|
|
||||||
attribute_role,
|
|
||||||
auto_signup_enabled,
|
|
||||||
domain_verified,
|
|
||||||
domain_verification_token,
|
|
||||||
domain_verified_at,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
FROM
|
|
||||||
auth_saml_configurations
|
|
||||||
WHERE
|
|
||||||
email_domain = $1
|
|
||||||
AND enabled = true
|
|
||||||
AND domain_verified = true
|
|
||||||
ORDER BY created_at ASC;
|
|
||||||
`
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, emailDomain)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot query auth_saml_configurations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
configs, err := pgx.CollectRows(rows, pgx.RowToStructByName[SAMLConfiguration])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot collect saml_configurations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]*SAMLConfiguration, len(configs))
|
|
||||||
for i := range configs {
|
|
||||||
result[i] = &configs[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadSAMLConfigurationsByOrganizationIDsAndEmailDomain loads SAML configurations for multiple organizations
|
|
||||||
// and a given email domain in a single query. This is used to avoid N+1 queries.
|
|
||||||
func LoadSAMLConfigurationsByOrganizationIDsAndEmailDomain(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
organizationIDs []gid.GID,
|
|
||||||
emailDomain string,
|
|
||||||
) (map[gid.GID]*SAMLConfiguration, error) {
|
|
||||||
if len(organizationIDs) == 0 {
|
|
||||||
return make(map[gid.GID]*SAMLConfiguration), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
email_domain,
|
|
||||||
enforcement_policy,
|
|
||||||
idp_entity_id,
|
|
||||||
idp_sso_url,
|
|
||||||
idp_certificate,
|
|
||||||
idp_metadata_url,
|
|
||||||
attribute_email,
|
|
||||||
attribute_firstname,
|
|
||||||
attribute_lastname,
|
|
||||||
attribute_role,
|
|
||||||
auto_signup_enabled,
|
|
||||||
domain_verified,
|
|
||||||
domain_verification_token,
|
|
||||||
domain_verified_at,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
FROM
|
|
||||||
auth_saml_configurations
|
|
||||||
WHERE
|
|
||||||
organization_id = ANY(@organization_ids)
|
|
||||||
AND email_domain = @email_domain
|
|
||||||
`
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"organization_ids": organizationIDs,
|
|
||||||
"email_domain": emailDomain,
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot query auth_saml_configurations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
configs, err := pgx.CollectRows(rows, pgx.RowToStructByName[SAMLConfiguration])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot collect saml_configurations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make(map[gid.GID]*SAMLConfiguration, len(configs))
|
|
||||||
for i := range configs {
|
|
||||||
result[configs[i].OrganizationID] = &configs[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SAMLConfigurations) CountByOrganizationID(
|
func (s *SAMLConfigurations) CountByOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
@@ -560,3 +485,50 @@ WHERE
|
|||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SAMLConfigurations) LoadUnverified(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
email_domain,
|
||||||
|
enforcement_policy,
|
||||||
|
idp_entity_id,
|
||||||
|
idp_sso_url,
|
||||||
|
idp_certificate,
|
||||||
|
idp_metadata_url,
|
||||||
|
attribute_email,
|
||||||
|
attribute_firstname,
|
||||||
|
attribute_lastname,
|
||||||
|
attribute_role,
|
||||||
|
auto_signup_enabled,
|
||||||
|
domain_verification_token,
|
||||||
|
domain_verified_at,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
auth_saml_configurations
|
||||||
|
WHERE
|
||||||
|
domain_verified_at IS NULL
|
||||||
|
AND domain_verification_token IS NOT NULL
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
LIMIT 100;
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query unverified auth_saml_configurations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configs, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[SAMLConfiguration])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect unverified saml_configurations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = configs
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -972,6 +972,7 @@ func (s OrganizationService) CreateSAMLConfiguration(
|
|||||||
var (
|
var (
|
||||||
now = time.Now()
|
now = time.Now()
|
||||||
scope = coredata.NewScopeFromObjectID(organizationID)
|
scope = coredata.NewScopeFromObjectID(organizationID)
|
||||||
|
domainVerificationToken = uuid.MustNewV4().String()
|
||||||
config = &coredata.SAMLConfiguration{
|
config = &coredata.SAMLConfiguration{
|
||||||
ID: gid.New(scope.GetTenantID(), coredata.SAMLConfigurationEntityType),
|
ID: gid.New(scope.GetTenantID(), coredata.SAMLConfigurationEntityType),
|
||||||
OrganizationID: organizationID,
|
OrganizationID: organizationID,
|
||||||
@@ -979,6 +980,7 @@ func (s OrganizationService) CreateSAMLConfiguration(
|
|||||||
IdPEntityID: req.IdPEntityID,
|
IdPEntityID: req.IdPEntityID,
|
||||||
IdPSsoURL: req.IdPSsoURL,
|
IdPSsoURL: req.IdPSsoURL,
|
||||||
IdPCertificate: req.IdPCertificate,
|
IdPCertificate: req.IdPCertificate,
|
||||||
|
DomainVerificationToken: &domainVerificationToken,
|
||||||
EmailDomain: req.EmailDomain,
|
EmailDomain: req.EmailDomain,
|
||||||
AutoSignupEnabled: req.AutoSignupEnabled,
|
AutoSignupEnabled: req.AutoSignupEnabled,
|
||||||
AttributeEmail: DefaultAttributeEmail,
|
AttributeEmail: DefaultAttributeEmail,
|
||||||
@@ -988,8 +990,6 @@ func (s OrganizationService) CreateSAMLConfiguration(
|
|||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO create domain verification object
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if req.AttributeEmail != nil {
|
if req.AttributeEmail != nil {
|
||||||
@@ -1059,7 +1059,7 @@ func (s OrganizationService) UpdateSAMLConfiguration(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.EnforcementPolicy != nil {
|
if req.EnforcementPolicy != nil {
|
||||||
if !config.DomainVerified {
|
if config.DomainVerifiedAt == nil {
|
||||||
return NewSAMLConfigurationDomainNotVerifiedError(configID)
|
return NewSAMLConfigurationDomainNotVerifiedError(configID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package iam
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
pemutil "go.probo.inc/probo/pkg/crypto/pem"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidateIdPConfiguration validates only the IdP (Identity Provider) configuration.
|
|
||||||
// This validates user-provided data from the IdP.
|
|
||||||
// SP (Service Provider) configuration is generated by the application and doesn't need validation.
|
|
||||||
func ValidateIdPConfiguration(
|
|
||||||
idpEntityID string,
|
|
||||||
idpSsoURL string,
|
|
||||||
idpCertificate string,
|
|
||||||
) error {
|
|
||||||
// Validate IdP Entity ID
|
|
||||||
if idpEntityID == "" {
|
|
||||||
return fmt.Errorf("IdP Entity ID cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate IdP SSO URL - accept both HTTP and HTTPS
|
|
||||||
if err := validateURL(idpSsoURL); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate IdP certificate
|
|
||||||
if err := validateCertificate(idpCertificate); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateURL(urlStr string) error {
|
|
||||||
if urlStr == "" {
|
|
||||||
return fmt.Errorf("URL cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedURL, err := url.Parse(urlStr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid URL format: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedURL.Scheme == "" {
|
|
||||||
return fmt.Errorf("URL must have a scheme (http or https)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
|
||||||
return fmt.Errorf("URL scheme must be http or https (found: %s)", parsedURL.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedURL.Host == "" {
|
|
||||||
return fmt.Errorf("URL must have a host")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateCertificate(certPEM string) error {
|
|
||||||
if certPEM == "" {
|
|
||||||
return fmt.Errorf("certificate cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
block, _ := pem.Decode([]byte(certPEM))
|
|
||||||
if block == nil {
|
|
||||||
return fmt.Errorf("cannot parse certificate PEM")
|
|
||||||
}
|
|
||||||
|
|
||||||
if block.Type != pemutil.BlockTypeCertificate {
|
|
||||||
return fmt.Errorf("PEM block type must be CERTIFICATE (found: %s)", block.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := x509.ParseCertificate(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot parse X.509 certificate: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
206
pkg/iam/saml_domain_verifier.go
Normal file
206
pkg/iam/saml_domain_verifier.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package iam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/miekg/dns"
|
||||||
|
"go.gearno.de/kit/log"
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
SAMLDomainVerifier struct {
|
||||||
|
pg *pg.Client
|
||||||
|
interval time.Duration
|
||||||
|
resolverAddr string
|
||||||
|
logger *log.Logger
|
||||||
|
tracer trace.Tracer
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
txtRecordValuePrefix = "probo-verification="
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSAMLDomainVerifier(
|
||||||
|
pgClient *pg.Client,
|
||||||
|
logger *log.Logger,
|
||||||
|
tp trace.TracerProvider,
|
||||||
|
interval time.Duration,
|
||||||
|
resolverAddr string,
|
||||||
|
) *SAMLDomainVerifier {
|
||||||
|
return &SAMLDomainVerifier{
|
||||||
|
pg: pgClient,
|
||||||
|
interval: interval,
|
||||||
|
resolverAddr: resolverAddr,
|
||||||
|
logger: logger.Named("saml-domain-verifier"),
|
||||||
|
tracer: tp.Tracer("go.probo.inc/probo/pkg/iam/saml_domain_verifier"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SAMLDomainVerifier) Run(ctx context.Context) error {
|
||||||
|
v.logger.InfoCtx(ctx, "starting", log.Duration("interval", v.interval))
|
||||||
|
|
||||||
|
for {
|
||||||
|
v.runOnce(ctx)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
v.logger.InfoCtx(ctx, "shutting down")
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(v.interval):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SAMLDomainVerifier) runOnce(ctx context.Context) {
|
||||||
|
ctx, span := v.tracer.Start(ctx, "SAMLDomainVerifier.runOnce")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
if err := v.checkUnverifiedDomains(ctx); err != nil {
|
||||||
|
v.logger.ErrorCtx(ctx, "cannot check unverified domains", log.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SAMLDomainVerifier) checkUnverifiedDomains(ctx context.Context) error {
|
||||||
|
var configs coredata.SAMLConfigurations
|
||||||
|
|
||||||
|
err := v.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
err := configs.LoadUnverified(ctx, conn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot load unverified SAML configurations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, config := range configs {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := v.tryVerifyDomain(ctx, config.ID); err != nil {
|
||||||
|
v.logger.ErrorCtx(ctx, "cannot verify domain",
|
||||||
|
log.String("config_id", config.ID.String()),
|
||||||
|
log.Error(err),
|
||||||
|
)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SAMLDomainVerifier) tryVerifyDomain(ctx context.Context, configID gid.GID) error {
|
||||||
|
return v.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
config := &coredata.SAMLConfiguration{}
|
||||||
|
if err := config.LoadByIDForUpdateSkipLocked(ctx, tx, configID); err != nil {
|
||||||
|
if err == coredata.ErrResourceNotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot load SAML configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DomainVerifiedAt != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DomainVerificationToken == nil {
|
||||||
|
return fmt.Errorf("cannot verify domain %q: no verification token", config.EmailDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedValue := txtRecordValuePrefix + *config.DomainVerificationToken
|
||||||
|
|
||||||
|
if err := v.checkDNSTXTRecord(config.EmailDomain, expectedValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.logger.InfoCtx(ctx, "domain verified",
|
||||||
|
log.String("config_id", config.ID.String()),
|
||||||
|
)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
config.DomainVerificationToken = nil
|
||||||
|
config.DomainVerifiedAt = &now
|
||||||
|
config.UpdatedAt = now
|
||||||
|
|
||||||
|
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||||
|
if err := config.Update(ctx, tx, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update SAML configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SAMLDomainVerifier) checkDNSTXTRecord(emailDomain string, expectedValue string) error {
|
||||||
|
fqdn := emailDomain
|
||||||
|
if !strings.HasSuffix(fqdn, ".") {
|
||||||
|
fqdn = fqdn + "."
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &dns.Msg{MsgHeader: dns.MsgHeader{ID: dns.ID(), RecursionDesired: true}}
|
||||||
|
msg.Question = []dns.RR{&dns.TXT{Hdr: dns.Header{Name: fqdn, Class: dns.ClassINET}}}
|
||||||
|
|
||||||
|
client := dns.NewClient()
|
||||||
|
resp, _, err := client.Exchange(context.Background(), msg, "udp", v.resolverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query TXT record for %q: %w", emailDomain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Rcode != dns.RcodeSuccess {
|
||||||
|
return fmt.Errorf("cannot query TXT record for %q: %s", emailDomain, dns.RcodeToString[resp.Rcode])
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Answer) == 0 {
|
||||||
|
return fmt.Errorf("cannot find TXT record for %q", emailDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, answer := range resp.Answer {
|
||||||
|
txt, ok := answer.(*dns.TXT)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value := strings.Join(txt.Txt, "")
|
||||||
|
|
||||||
|
if value == expectedValue {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot find matching TXT record for %q", emailDomain)
|
||||||
|
}
|
||||||
@@ -9,12 +9,14 @@ import (
|
|||||||
|
|
||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/crypto/cipher"
|
"go.probo.inc/probo/pkg/crypto/cipher"
|
||||||
"go.probo.inc/probo/pkg/crypto/passwdhash"
|
"go.probo.inc/probo/pkg/crypto/passwdhash"
|
||||||
"go.probo.inc/probo/pkg/filemanager"
|
"go.probo.inc/probo/pkg/filemanager"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
"go.probo.inc/probo/pkg/iam/saml"
|
"go.probo.inc/probo/pkg/iam/saml"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -42,6 +44,8 @@ type (
|
|||||||
APIKeyService *APIKeyService
|
APIKeyService *APIKeyService
|
||||||
LegacyAccessManagementService *AccessManagementService
|
LegacyAccessManagementService *AccessManagementService
|
||||||
Authorizer *Authorizer
|
Authorizer *Authorizer
|
||||||
|
|
||||||
|
samlDomainVerifier *SAMLDomainVerifier
|
||||||
}
|
}
|
||||||
|
|
||||||
Config struct {
|
Config struct {
|
||||||
@@ -57,6 +61,9 @@ type (
|
|||||||
PrivateKey *rsa.PrivateKey
|
PrivateKey *rsa.PrivateKey
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
PolicySet *PolicySet
|
PolicySet *PolicySet
|
||||||
|
TracerProvider trace.TracerProvider
|
||||||
|
DomainVerificationInterval time.Duration
|
||||||
|
DomainVerificationResolverAddr string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -122,31 +129,24 @@ func NewService(
|
|||||||
}
|
}
|
||||||
svc.SAMLService = samlService
|
svc.SAMLService = samlService
|
||||||
|
|
||||||
|
svc.samlDomainVerifier = NewSAMLDomainVerifier(
|
||||||
|
pgClient,
|
||||||
|
cfg.Logger,
|
||||||
|
cfg.TracerProvider,
|
||||||
|
cfg.DomainVerificationInterval,
|
||||||
|
cfg.DomainVerificationResolverAddr,
|
||||||
|
)
|
||||||
|
|
||||||
return svc, nil
|
return svc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Run(ctx context.Context) error {
|
func (s *Service) Run(ctx context.Context) error {
|
||||||
runCtx, stopAll := context.WithCancel(ctx)
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
defer stopAll()
|
|
||||||
|
|
||||||
errCh := make(chan error, 1)
|
g.Go(func() error { return s.SAMLService.Run(ctx) })
|
||||||
go func() {
|
g.Go(func() error { return s.samlDomainVerifier.Run(ctx) })
|
||||||
errCh <- s.SAMLService.Run(runCtx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
return g.Wait()
|
||||||
case <-ctx.Done():
|
|
||||||
stopAll()
|
|
||||||
<-errCh
|
|
||||||
return ctx.Err()
|
|
||||||
case err := <-errCh:
|
|
||||||
if err != nil {
|
|
||||||
s.logger.ErrorCtx(ctx, "iam service failed", log.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetMembership(ctx context.Context, membershipID gid.GID) (*coredata.Membership, error) {
|
func (s *Service) GetMembership(ctx context.Context, membershipID gid.GID) (*coredata.Membership, error) {
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ func New() *Implm {
|
|||||||
SAML: samlConfig{
|
SAML: samlConfig{
|
||||||
SessionDuration: 604800,
|
SessionDuration: 604800,
|
||||||
CleanupIntervalSeconds: 86400,
|
CleanupIntervalSeconds: 86400,
|
||||||
|
DomainVerificationIntervalSeconds: 60,
|
||||||
|
DomainVerificationResolverAddr: "8.8.8.8:53",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
TrustAuth: trustAuthConfig{
|
TrustAuth: trustAuthConfig{
|
||||||
@@ -324,6 +326,9 @@ func (impl *Implm) Run(
|
|||||||
Certificate: samlCert,
|
Certificate: samlCert,
|
||||||
PrivateKey: samlKey,
|
PrivateKey: samlKey,
|
||||||
Logger: l.Named("iam"),
|
Logger: l.Named("iam"),
|
||||||
|
TracerProvider: tp,
|
||||||
|
DomainVerificationInterval: impl.cfg.Auth.SAML.DomainVerificationInterval(),
|
||||||
|
DomainVerificationResolverAddr: impl.cfg.Auth.SAML.DomainVerificationResolverAddr,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -494,11 +499,9 @@ func (impl *Implm) Run(
|
|||||||
iamServiceCtx, stopIAMService := context.WithCancel(context.Background())
|
iamServiceCtx, stopIAMService := context.WithCancel(context.Background())
|
||||||
wg.Go(
|
wg.Go(
|
||||||
func() {
|
func() {
|
||||||
|
|
||||||
if err := iamService.Run(iamServiceCtx); err != nil {
|
if err := iamService.Run(iamServiceCtx); err != nil {
|
||||||
cancel(fmt.Errorf("iam service crashed: %w", err))
|
cancel(fmt.Errorf("iam service crashed: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ type samlConfig struct {
|
|||||||
CleanupIntervalSeconds int `json:"cleanup-interval-seconds"`
|
CleanupIntervalSeconds int `json:"cleanup-interval-seconds"`
|
||||||
Certificate string `json:"certificate"`
|
Certificate string `json:"certificate"`
|
||||||
PrivateKey string `json:"private-key"`
|
PrivateKey string `json:"private-key"`
|
||||||
|
DomainVerificationIntervalSeconds int `json:"domain-verification-interval-seconds"`
|
||||||
|
DomainVerificationResolverAddr string `json:"domain-verification-resolver-addr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c samlConfig) SessionDurationTime() time.Duration {
|
func (c samlConfig) SessionDurationTime() time.Duration {
|
||||||
@@ -36,5 +38,10 @@ func (c samlConfig) CleanupInterval() time.Duration {
|
|||||||
if c.CleanupIntervalSeconds == 0 {
|
if c.CleanupIntervalSeconds == 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return time.Duration(c.CleanupIntervalSeconds) * time.Second
|
return time.Duration(c.CleanupIntervalSeconds) * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c samlConfig) DomainVerificationInterval() time.Duration {
|
||||||
|
return time.Duration(c.DomainVerificationIntervalSeconds) * time.Second
|
||||||
|
}
|
||||||
|
|||||||
@@ -318,9 +318,7 @@ type SessionPolicy {
|
|||||||
type SAMLConfiguration implements Node {
|
type SAMLConfiguration implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
emailDomain: String!
|
emailDomain: String!
|
||||||
enabled: Boolean!
|
|
||||||
enforcementPolicy: SAMLEnforcementPolicy!
|
enforcementPolicy: SAMLEnforcementPolicy!
|
||||||
domainVerified: Boolean!
|
|
||||||
domainVerifiedAt: Datetime
|
domainVerifiedAt: Datetime
|
||||||
domainVerificationToken: String
|
domainVerificationToken: String
|
||||||
idpEntityId: String!
|
idpEntityId: String!
|
||||||
|
|||||||
@@ -355,10 +355,8 @@ type ComplexityRoot struct {
|
|||||||
CreatedAt func(childComplexity int) int
|
CreatedAt func(childComplexity int) int
|
||||||
DefaultPermissions func(childComplexity int) int
|
DefaultPermissions func(childComplexity int) int
|
||||||
DomainVerificationToken func(childComplexity int) int
|
DomainVerificationToken func(childComplexity int) int
|
||||||
DomainVerified func(childComplexity int) int
|
|
||||||
DomainVerifiedAt func(childComplexity int) int
|
DomainVerifiedAt func(childComplexity int) int
|
||||||
EmailDomain func(childComplexity int) int
|
EmailDomain func(childComplexity int) int
|
||||||
Enabled func(childComplexity int) int
|
|
||||||
EnforcementPolicy func(childComplexity int) int
|
EnforcementPolicy func(childComplexity int) int
|
||||||
ID func(childComplexity int) int
|
ID func(childComplexity int) int
|
||||||
IdpCertificate func(childComplexity int) int
|
IdpCertificate func(childComplexity int) int
|
||||||
@@ -1733,12 +1731,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
return e.complexity.SAMLConfiguration.DomainVerificationToken(childComplexity), true
|
return e.complexity.SAMLConfiguration.DomainVerificationToken(childComplexity), true
|
||||||
case "SAMLConfiguration.domainVerified":
|
|
||||||
if e.complexity.SAMLConfiguration.DomainVerified == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.SAMLConfiguration.DomainVerified(childComplexity), true
|
|
||||||
case "SAMLConfiguration.domainVerifiedAt":
|
case "SAMLConfiguration.domainVerifiedAt":
|
||||||
if e.complexity.SAMLConfiguration.DomainVerifiedAt == nil {
|
if e.complexity.SAMLConfiguration.DomainVerifiedAt == nil {
|
||||||
break
|
break
|
||||||
@@ -1751,12 +1743,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
return e.complexity.SAMLConfiguration.EmailDomain(childComplexity), true
|
return e.complexity.SAMLConfiguration.EmailDomain(childComplexity), true
|
||||||
case "SAMLConfiguration.enabled":
|
|
||||||
if e.complexity.SAMLConfiguration.Enabled == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.SAMLConfiguration.Enabled(childComplexity), true
|
|
||||||
case "SAMLConfiguration.enforcementPolicy":
|
case "SAMLConfiguration.enforcementPolicy":
|
||||||
if e.complexity.SAMLConfiguration.EnforcementPolicy == nil {
|
if e.complexity.SAMLConfiguration.EnforcementPolicy == nil {
|
||||||
break
|
break
|
||||||
@@ -2484,9 +2470,7 @@ type SessionPolicy {
|
|||||||
type SAMLConfiguration implements Node {
|
type SAMLConfiguration implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
emailDomain: String!
|
emailDomain: String!
|
||||||
enabled: Boolean!
|
|
||||||
enforcementPolicy: SAMLEnforcementPolicy!
|
enforcementPolicy: SAMLEnforcementPolicy!
|
||||||
domainVerified: Boolean!
|
|
||||||
domainVerifiedAt: Datetime
|
domainVerifiedAt: Datetime
|
||||||
domainVerificationToken: String
|
domainVerificationToken: String
|
||||||
idpEntityId: String!
|
idpEntityId: String!
|
||||||
@@ -10090,35 +10074,6 @@ func (ec *executionContext) fieldContext_SAMLConfiguration_emailDomain(_ context
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _SAMLConfiguration_enabled(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
|
|
||||||
return graphql.ResolveField(
|
|
||||||
ctx,
|
|
||||||
ec.OperationContext,
|
|
||||||
field,
|
|
||||||
ec.fieldContext_SAMLConfiguration_enabled,
|
|
||||||
func(ctx context.Context) (any, error) {
|
|
||||||
return obj.Enabled, nil
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
ec.marshalNBoolean2bool,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_SAMLConfiguration_enabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
|
||||||
fc = &graphql.FieldContext{
|
|
||||||
Object: "SAMLConfiguration",
|
|
||||||
Field: field,
|
|
||||||
IsMethod: false,
|
|
||||||
IsResolver: false,
|
|
||||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
|
||||||
return nil, errors.New("field of type Boolean does not have child fields")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return fc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) _SAMLConfiguration_enforcementPolicy(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
|
func (ec *executionContext) _SAMLConfiguration_enforcementPolicy(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -10148,35 +10103,6 @@ func (ec *executionContext) fieldContext_SAMLConfiguration_enforcementPolicy(_ c
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _SAMLConfiguration_domainVerified(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
|
|
||||||
return graphql.ResolveField(
|
|
||||||
ctx,
|
|
||||||
ec.OperationContext,
|
|
||||||
field,
|
|
||||||
ec.fieldContext_SAMLConfiguration_domainVerified,
|
|
||||||
func(ctx context.Context) (any, error) {
|
|
||||||
return obj.DomainVerified, nil
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
ec.marshalNBoolean2bool,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_SAMLConfiguration_domainVerified(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
|
||||||
fc = &graphql.FieldContext{
|
|
||||||
Object: "SAMLConfiguration",
|
|
||||||
Field: field,
|
|
||||||
IsMethod: false,
|
|
||||||
IsResolver: false,
|
|
||||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
|
||||||
return nil, errors.New("field of type Boolean does not have child fields")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return fc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) _SAMLConfiguration_domainVerifiedAt(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
|
func (ec *executionContext) _SAMLConfiguration_domainVerifiedAt(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -10672,12 +10598,8 @@ func (ec *executionContext) fieldContext_SAMLConfigurationEdge_node(_ context.Co
|
|||||||
return ec.fieldContext_SAMLConfiguration_id(ctx, field)
|
return ec.fieldContext_SAMLConfiguration_id(ctx, field)
|
||||||
case "emailDomain":
|
case "emailDomain":
|
||||||
return ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
|
return ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
|
||||||
case "enabled":
|
|
||||||
return ec.fieldContext_SAMLConfiguration_enabled(ctx, field)
|
|
||||||
case "enforcementPolicy":
|
case "enforcementPolicy":
|
||||||
return ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
|
return ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
|
||||||
case "domainVerified":
|
|
||||||
return ec.fieldContext_SAMLConfiguration_domainVerified(ctx, field)
|
|
||||||
case "domainVerifiedAt":
|
case "domainVerifiedAt":
|
||||||
return ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
|
return ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
|
||||||
case "domainVerificationToken":
|
case "domainVerificationToken":
|
||||||
@@ -11739,12 +11661,8 @@ func (ec *executionContext) fieldContext_UpdateSAMLConfigurationPayload_samlConf
|
|||||||
return ec.fieldContext_SAMLConfiguration_id(ctx, field)
|
return ec.fieldContext_SAMLConfiguration_id(ctx, field)
|
||||||
case "emailDomain":
|
case "emailDomain":
|
||||||
return ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
|
return ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
|
||||||
case "enabled":
|
|
||||||
return ec.fieldContext_SAMLConfiguration_enabled(ctx, field)
|
|
||||||
case "enforcementPolicy":
|
case "enforcementPolicy":
|
||||||
return ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
|
return ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
|
||||||
case "domainVerified":
|
|
||||||
return ec.fieldContext_SAMLConfiguration_domainVerified(ctx, field)
|
|
||||||
case "domainVerifiedAt":
|
case "domainVerifiedAt":
|
||||||
return ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
|
return ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
|
||||||
case "domainVerificationToken":
|
case "domainVerificationToken":
|
||||||
@@ -17312,21 +17230,11 @@ func (ec *executionContext) _SAMLConfiguration(ctx context.Context, sel ast.Sele
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
out.Invalids++
|
out.Invalids++
|
||||||
}
|
}
|
||||||
case "enabled":
|
|
||||||
out.Values[i] = ec._SAMLConfiguration_enabled(ctx, field, obj)
|
|
||||||
if out.Values[i] == graphql.Null {
|
|
||||||
out.Invalids++
|
|
||||||
}
|
|
||||||
case "enforcementPolicy":
|
case "enforcementPolicy":
|
||||||
out.Values[i] = ec._SAMLConfiguration_enforcementPolicy(ctx, field, obj)
|
out.Values[i] = ec._SAMLConfiguration_enforcementPolicy(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
out.Invalids++
|
out.Invalids++
|
||||||
}
|
}
|
||||||
case "domainVerified":
|
|
||||||
out.Values[i] = ec._SAMLConfiguration_domainVerified(ctx, field, obj)
|
|
||||||
if out.Values[i] == graphql.Null {
|
|
||||||
out.Invalids++
|
|
||||||
}
|
|
||||||
case "domainVerifiedAt":
|
case "domainVerifiedAt":
|
||||||
out.Values[i] = ec._SAMLConfiguration_domainVerifiedAt(ctx, field, obj)
|
out.Values[i] = ec._SAMLConfiguration_domainVerifiedAt(ctx, field, obj)
|
||||||
case "domainVerificationToken":
|
case "domainVerificationToken":
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ func NewSAMLConfiguration(samlConfiguration *coredata.SAMLConfiguration) *SAMLCo
|
|||||||
ID: samlConfiguration.ID,
|
ID: samlConfiguration.ID,
|
||||||
EmailDomain: samlConfiguration.EmailDomain,
|
EmailDomain: samlConfiguration.EmailDomain,
|
||||||
EnforcementPolicy: samlConfiguration.EnforcementPolicy,
|
EnforcementPolicy: samlConfiguration.EnforcementPolicy,
|
||||||
DomainVerified: samlConfiguration.DomainVerified,
|
|
||||||
DomainVerifiedAt: samlConfiguration.DomainVerifiedAt,
|
DomainVerifiedAt: samlConfiguration.DomainVerifiedAt,
|
||||||
DomainVerificationToken: samlConfiguration.DomainVerificationToken,
|
DomainVerificationToken: samlConfiguration.DomainVerificationToken,
|
||||||
IdpEntityID: samlConfiguration.IdPEntityID,
|
IdpEntityID: samlConfiguration.IdPEntityID,
|
||||||
IdpSsoURL: samlConfiguration.IdPSsoURL,
|
IdpSsoURL: samlConfiguration.IdPSsoURL,
|
||||||
IdpCertificate: samlConfiguration.IdPCertificate,
|
IdpCertificate: samlConfiguration.IdPCertificate,
|
||||||
|
AutoSignupEnabled: samlConfiguration.AutoSignupEnabled,
|
||||||
CreatedAt: samlConfiguration.CreatedAt,
|
CreatedAt: samlConfiguration.CreatedAt,
|
||||||
UpdatedAt: samlConfiguration.UpdatedAt,
|
UpdatedAt: samlConfiguration.UpdatedAt,
|
||||||
AttributeMappings: &SAMLAttributeMappings{
|
AttributeMappings: &SAMLAttributeMappings{
|
||||||
|
|||||||
@@ -400,9 +400,7 @@ func (SAMLAuthenticationRequired) IsAssumeOrganizationSessionResult() {}
|
|||||||
type SAMLConfiguration struct {
|
type SAMLConfiguration struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
EmailDomain string `json:"emailDomain"`
|
EmailDomain string `json:"emailDomain"`
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
|
EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
|
||||||
DomainVerified bool `json:"domainVerified"`
|
|
||||||
DomainVerifiedAt *time.Time `json:"domainVerifiedAt,omitempty"`
|
DomainVerifiedAt *time.Time `json:"domainVerifiedAt,omitempty"`
|
||||||
DomainVerificationToken *string `json:"domainVerificationToken,omitempty"`
|
DomainVerificationToken *string `json:"domainVerificationToken,omitempty"`
|
||||||
IdpEntityID string `json:"idpEntityId"`
|
IdpEntityID string `json:"idpEntityId"`
|
||||||
|
|||||||
Reference in New Issue
Block a user