@@ -4,13 +4,13 @@ import { useFragment, graphql } from "react-relay";
|
|||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
|
||||||
Field,
|
Field,
|
||||||
Label,
|
Label,
|
||||||
Option,
|
Option,
|
||||||
@@ -491,16 +491,23 @@ export default function SAMLSettingsTab() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog ref={dialogRef} onClose={handleCloseModal}>
|
<Dialog
|
||||||
<DialogContent>
|
ref={dialogRef}
|
||||||
<DialogTitle>
|
onClose={handleCloseModal}
|
||||||
{currentStep === "initiate" && __("Step 1: Register Domain")}
|
title={
|
||||||
{currentStep === "verify" && __("Step 2: Verify Domain Ownership")}
|
<Breadcrumb
|
||||||
{currentStep === "configure" && (editingConfig?.domainVerified ? __("Configure SAML") : __("Step 3: Configure SAML"))}
|
items={[
|
||||||
</DialogTitle>
|
__("SAML Settings"),
|
||||||
|
currentStep === "initiate" && __("Register Domain"),
|
||||||
|
currentStep === "verify" && __("Verify Domain"),
|
||||||
|
currentStep === "configure" && (editingConfig?.domainVerified ? __("Configure SAML") : __("Configure SAML")),
|
||||||
|
].filter(Boolean) as string[]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
{currentStep === "initiate" && (
|
{currentStep === "initiate" && (
|
||||||
<form onSubmit={handleInitiateDomain} className="space-y-6 p-6">
|
<form onSubmit={handleInitiateDomain}>
|
||||||
|
<DialogContent padded className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-medium mb-4">
|
<h3 className="text-base font-medium mb-4">
|
||||||
{__("Register Your Domain")}
|
{__("Register Your Domain")}
|
||||||
@@ -520,11 +527,18 @@ export default function SAMLSettingsTab() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" disabled={isInitiating}>
|
||||||
|
{__("Next: Verify Domain")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentStep === "verify" && (
|
{currentStep === "verify" && (
|
||||||
<div className="space-y-6 p-6">
|
<>
|
||||||
|
<DialogContent padded className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-medium mb-4">
|
<h3 className="text-base font-medium mb-4">
|
||||||
{__("Verify Domain Ownership")}
|
{__("Verify Domain Ownership")}
|
||||||
@@ -566,11 +580,18 @@ export default function SAMLSettingsTab() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DialogContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={handleVerifyDomain} disabled={isVerifying}>
|
||||||
|
{__("Verify and Continue")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentStep === "configure" && (
|
{currentStep === "configure" && (
|
||||||
<form onSubmit={onSubmit} className="space-y-6 p-6">
|
<form onSubmit={onSubmit}>
|
||||||
|
<DialogContent padded className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-medium mb-4">
|
<h3 className="text-base font-medium mb-4">
|
||||||
{__("Basic Configuration")}
|
{__("Basic Configuration")}
|
||||||
@@ -710,48 +731,14 @@ export default function SAMLSettingsTab() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</DialogContent>
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button type="submit" disabled={isCreating || isUpdating}>
|
||||||
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}
|
|
||||||
>
|
|
||||||
{editingConfig?.domainVerified ? __("Update Configuration") : __("Create Configuration")}
|
{editingConfig?.domainVerified ? __("Update Configuration") : __("Create Configuration")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</form>
|
||||||
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,28 +13,23 @@ type AuthMethod int
|
|||||||
const (
|
const (
|
||||||
AuthMethodPassword AuthMethod = iota
|
AuthMethodPassword AuthMethod = iota
|
||||||
AuthMethodSAML
|
AuthMethodSAML
|
||||||
AuthMethodAny // Used when either password or SAML would work
|
AuthMethodAny
|
||||||
)
|
)
|
||||||
|
|
||||||
// OrgAuthRequirement encapsulates the authentication requirements for accessing an organization
|
|
||||||
type OrgAuthRequirement struct {
|
type OrgAuthRequirement struct {
|
||||||
OrganizationID gid.GID
|
OrganizationID gid.GID
|
||||||
EmailDomain string
|
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 {
|
type AccessResult struct {
|
||||||
OrganizationID gid.GID
|
OrganizationID gid.GID
|
||||||
Allowed bool
|
Allowed bool
|
||||||
MissingAuth AuthMethod // Which auth method is missing (if not allowed)
|
MissingAuth AuthMethod
|
||||||
SAMLConfig *coredata.SAMLConfiguration // The SAML config involved (if any)
|
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 {
|
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 {
|
if r.SAMLConfig == nil || !r.SAMLConfig.Enabled || !r.SAMLConfig.DomainVerified {
|
||||||
return AccessResult{
|
return AccessResult{
|
||||||
OrganizationID: r.OrganizationID,
|
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()
|
orgKey := r.OrganizationID.String()
|
||||||
_, hasSAML := session.SAMLAuthenticatedOrgs[orgKey]
|
_, hasSAML := session.SAMLAuthenticatedOrgs[orgKey]
|
||||||
|
|
||||||
// SAML enforcement: REQUIRED → must have SAML auth for this org
|
|
||||||
if r.SAMLConfig.EnforcementPolicy == coredata.SAMLEnforcementPolicyRequired {
|
if r.SAMLConfig.EnforcementPolicy == coredata.SAMLEnforcementPolicyRequired {
|
||||||
return AccessResult{
|
return AccessResult{
|
||||||
OrganizationID: r.OrganizationID,
|
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
|
hasAnyAuth := session.PasswordAuthenticated || hasSAML
|
||||||
missingAuth := AuthMethodAny
|
missingAuth := AuthMethodAny
|
||||||
if hasAnyAuth {
|
if hasAnyAuth {
|
||||||
missingAuth = AuthMethodPassword // Not actually missing, but need a value
|
missingAuth = AuthMethodPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
return AccessResult{
|
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 {
|
func (r AccessResult) ToError(baseURL string) error {
|
||||||
if r.Allowed {
|
if r.Allowed {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ func (c *Cleaner) Run(ctx context.Context) error {
|
|||||||
func (c *Cleaner) cleanup(ctx context.Context) error {
|
func (c *Cleaner) cleanup(ctx context.Context) error {
|
||||||
var assertionsDeleted, requestsDeleted, relayStatesDeleted int64
|
var assertionsDeleted, requestsDeleted, relayStatesDeleted int64
|
||||||
|
|
||||||
err := c.pg.WithConn(ctx, func(conn pg.Conn) error {
|
err := c.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
count, err := CleanupExpiredAssertions(ctx, conn)
|
count, err := CleanupExpiredAssertions(ctx, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -96,7 +98,8 @@ func (c *Cleaner) cleanup(ctx context.Context) error {
|
|||||||
relayStatesDeleted = count
|
relayStatesDeleted = count
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -153,52 +153,3 @@ WHERE %s
|
|||||||
|
|
||||||
return err
|
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)
|
authMethod, err := auth.GetUserAuthMethod(ctx, obj.UserID, obj.OrganizationID, session)
|
||||||
if err != nil {
|
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
|
return authMethod, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user