@@ -4,13 +4,13 @@ import { useFragment, graphql } from "react-relay";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
Field,
|
||||
Label,
|
||||
Option,
|
||||
@@ -491,16 +491,23 @@ export default function SAMLSettingsTab() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog ref={dialogRef} onClose={handleCloseModal}>
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
{currentStep === "initiate" && __("Step 1: Register Domain")}
|
||||
{currentStep === "verify" && __("Step 2: Verify Domain Ownership")}
|
||||
{currentStep === "configure" && (editingConfig?.domainVerified ? __("Configure SAML") : __("Step 3: Configure SAML"))}
|
||||
</DialogTitle>
|
||||
|
||||
{currentStep === "initiate" && (
|
||||
<form onSubmit={handleInitiateDomain} className="space-y-6 p-6">
|
||||
<Dialog
|
||||
ref={dialogRef}
|
||||
onClose={handleCloseModal}
|
||||
title={
|
||||
<Breadcrumb
|
||||
items={[
|
||||
__("SAML Settings"),
|
||||
currentStep === "initiate" && __("Register Domain"),
|
||||
currentStep === "verify" && __("Verify Domain"),
|
||||
currentStep === "configure" && (editingConfig?.domainVerified ? __("Configure SAML") : __("Configure SAML")),
|
||||
].filter(Boolean) as string[]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{currentStep === "initiate" && (
|
||||
<form onSubmit={handleInitiateDomain}>
|
||||
<DialogContent padded className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-base font-medium mb-4">
|
||||
{__("Register Your Domain")}
|
||||
@@ -520,11 +527,18 @@ export default function SAMLSettingsTab() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isInitiating}>
|
||||
{__("Next: Verify Domain")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{currentStep === "verify" && (
|
||||
<div className="space-y-6 p-6">
|
||||
{currentStep === "verify" && (
|
||||
<>
|
||||
<DialogContent padded className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-base font-medium mb-4">
|
||||
{__("Verify Domain Ownership")}
|
||||
@@ -566,12 +580,19 @@ export default function SAMLSettingsTab() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleVerifyDomain} disabled={isVerifying}>
|
||||
{__("Verify and Continue")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentStep === "configure" && (
|
||||
<form onSubmit={onSubmit} className="space-y-6 p-6">
|
||||
<div>
|
||||
{currentStep === "configure" && (
|
||||
<form onSubmit={onSubmit}>
|
||||
<DialogContent padded className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-base font-medium mb-4">
|
||||
{__("Basic Configuration")}
|
||||
</h3>
|
||||
@@ -710,48 +731,14 @@ export default function SAMLSettingsTab() {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleCloseModal}
|
||||
disabled={isCreating || isUpdating || isInitiating || isVerifying}
|
||||
>
|
||||
{__("Cancel")}
|
||||
</Button>
|
||||
|
||||
{currentStep === "initiate" && (
|
||||
<Button
|
||||
onClick={handleInitiateDomain}
|
||||
disabled={isInitiating}
|
||||
>
|
||||
{__("Next: Verify Domain")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentStep === "verify" && (
|
||||
<Button
|
||||
onClick={handleVerifyDomain}
|
||||
disabled={isVerifying}
|
||||
>
|
||||
{__("Verify and Continue")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentStep === "configure" && (
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
disabled={isCreating || isUpdating}
|
||||
>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isCreating || isUpdating}>
|
||||
{editingConfig?.domainVerified ? __("Update Configuration") : __("Create Configuration")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
)}
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -13,28 +13,23 @@ type AuthMethod int
|
||||
const (
|
||||
AuthMethodPassword AuthMethod = iota
|
||||
AuthMethodSAML
|
||||
AuthMethodAny // Used when either password or SAML would work
|
||||
AuthMethodAny
|
||||
)
|
||||
|
||||
// OrgAuthRequirement encapsulates the authentication requirements for accessing an organization
|
||||
type OrgAuthRequirement struct {
|
||||
OrganizationID gid.GID
|
||||
EmailDomain string
|
||||
SAMLConfig *coredata.SAMLConfiguration // nil if no SAML config applies to this org+domain
|
||||
SAMLConfig *coredata.SAMLConfiguration
|
||||
}
|
||||
|
||||
// AccessResult represents the result of an organization access check
|
||||
type AccessResult struct {
|
||||
OrganizationID gid.GID
|
||||
Allowed bool
|
||||
MissingAuth AuthMethod // Which auth method is missing (if not allowed)
|
||||
SAMLConfig *coredata.SAMLConfiguration // The SAML config involved (if any)
|
||||
MissingAuth AuthMethod
|
||||
SAMLConfig *coredata.SAMLConfiguration
|
||||
}
|
||||
|
||||
// Check performs the access control decision based on session state
|
||||
// This is pure business logic with no side effects - easily testable
|
||||
func (r OrgAuthRequirement) Check(session coredata.SessionData) AccessResult {
|
||||
// No SAML config or disabled → requires password authentication
|
||||
if r.SAMLConfig == nil || !r.SAMLConfig.Enabled || !r.SAMLConfig.DomainVerified {
|
||||
return AccessResult{
|
||||
OrganizationID: r.OrganizationID,
|
||||
@@ -44,11 +39,9 @@ func (r OrgAuthRequirement) Check(session coredata.SessionData) AccessResult {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user has SAML authentication for this specific organization
|
||||
orgKey := r.OrganizationID.String()
|
||||
_, hasSAML := session.SAMLAuthenticatedOrgs[orgKey]
|
||||
|
||||
// SAML enforcement: REQUIRED → must have SAML auth for this org
|
||||
if r.SAMLConfig.EnforcementPolicy == coredata.SAMLEnforcementPolicyRequired {
|
||||
return AccessResult{
|
||||
OrganizationID: r.OrganizationID,
|
||||
@@ -58,11 +51,10 @@ func (r OrgAuthRequirement) Check(session coredata.SessionData) AccessResult {
|
||||
}
|
||||
}
|
||||
|
||||
// SAML enforcement: OPTIONAL → needs either password (global) OR SAML (for this org)
|
||||
hasAnyAuth := session.PasswordAuthenticated || hasSAML
|
||||
missingAuth := AuthMethodAny
|
||||
if hasAnyAuth {
|
||||
missingAuth = AuthMethodPassword // Not actually missing, but need a value
|
||||
missingAuth = AuthMethodPassword
|
||||
}
|
||||
|
||||
return AccessResult{
|
||||
@@ -73,8 +65,6 @@ func (r OrgAuthRequirement) Check(session coredata.SessionData) AccessResult {
|
||||
}
|
||||
}
|
||||
|
||||
// ToError converts an AccessResult to an error if access is denied
|
||||
// This handles the presentation layer concern of generating appropriate errors and redirect URLs
|
||||
func (r AccessResult) ToError(baseURL string) error {
|
||||
if r.Allowed {
|
||||
return nil
|
||||
|
||||
@@ -76,27 +76,30 @@ func (c *Cleaner) Run(ctx context.Context) error {
|
||||
func (c *Cleaner) cleanup(ctx context.Context) error {
|
||||
var assertionsDeleted, requestsDeleted, relayStatesDeleted int64
|
||||
|
||||
err := c.pg.WithConn(ctx, func(conn pg.Conn) error {
|
||||
count, err := CleanupExpiredAssertions(ctx, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
assertionsDeleted = count
|
||||
err := c.pg.WithConn(
|
||||
ctx,
|
||||
func(conn pg.Conn) error {
|
||||
count, err := CleanupExpiredAssertions(ctx, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
assertionsDeleted = count
|
||||
|
||||
count, err = CleanupExpiredRequests(ctx, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requestsDeleted = count
|
||||
count, err = CleanupExpiredRequests(ctx, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requestsDeleted = count
|
||||
|
||||
count, err = CleanupExpiredRelayStates(ctx, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relayStatesDeleted = count
|
||||
count, err = CleanupExpiredRelayStates(ctx, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relayStatesDeleted = count
|
||||
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -153,52 +153,3 @@ WHERE %s
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadFilesByIDs loads multiple files by their IDs in a single query
|
||||
// Returns a map of file ID to File for efficient lookup
|
||||
func LoadFilesByIDs(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
fileIDs []gid.GID,
|
||||
) (map[gid.GID]*File, error) {
|
||||
if len(fileIDs) == 0 {
|
||||
return make(map[gid.GID]*File), nil
|
||||
}
|
||||
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
bucket_name,
|
||||
mime_type,
|
||||
file_name,
|
||||
file_key,
|
||||
file_size,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM
|
||||
files
|
||||
WHERE
|
||||
id = ANY(@file_ids)
|
||||
AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{"file_ids": fileIDs}
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot query files: %w", err)
|
||||
}
|
||||
|
||||
files, err := pgx.CollectRows(rows, pgx.RowToStructByName[File])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot collect files: %w", err)
|
||||
}
|
||||
|
||||
result := make(map[gid.GID]*File, len(files))
|
||||
for i := range files {
|
||||
result[files[i].ID] = &files[i]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -1080,7 +1080,7 @@ func (r *membershipResolver) AuthMethod(ctx context.Context, obj *types.Membersh
|
||||
|
||||
authMethod, err := auth.GetUserAuthMethod(ctx, obj.UserID, obj.OrganizationID, session)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot get user auth method: %w", err)
|
||||
panic(fmt.Errorf("cannot get user auth method: %w", err))
|
||||
}
|
||||
return authMethod, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user