Skip to content

Commit

Permalink
feat: Copy ACS URL for SAML configurations to clipboard. Disable edit…
Browse files Browse the repository at this point in the history
…ing SAML configuration names (#4494)

Co-authored-by: kyle-ssg <[email protected]>
  • Loading branch information
rolodato and kyle-ssg authored Aug 20, 2024
1 parent 5cfdaba commit 3f561ee
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 34 deletions.
24 changes: 13 additions & 11 deletions docs/docs/system-administration/authentication/01-SAML/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ SAML tab, you'll be able to configure it.

In the UI, you will be able to configure the following fields.

**Name:** (**Required**) A short name for the organisation, used as the input when clicking "Single Sign-on" at login
(note this is unique across all tenants and will form part of the URL so should only be alphanumeric + '-,\_').
**Name:** (**Required**) A short name for the organisation, used as the input when clicking "Single Sign-On" at login.
This name must be unique across all Flagsmith organisations and forms part of the URL that your identity provider will
post SAML messages to during authentication.

**Frontend URL**: (**Required**) This should be the base URL of the Flagsmith dashboard.
**Frontend URL**: (**Required**) This should be the base URL of the Flagsmith dashboard. Users will be redirected here
after authenticating successfully.

**Allow IdP initiated**: This field determines whether logins can be initiated from the IdP.
**Allow IdP-initiated**: If enabled, users will be able to log in directly from your identity provider without needing
to visit the Flagsmith login page.

**IdP metadata xml**: The metadata from the IdP.
**IdP metadata XML**: The metadata from your identity provider.

Once you have configured your identity provider, you can download the service provider metadata XML document with the
button "Download Service Provider Metadata".

### Assertion Consumer Service URL
### Assertion consumer service URL

The assertion consumer service (ACS) URL, also known as single sign-on URL, for this SAML configuration will be at the
following path, replacing `flagsmith.example.com` with your Flagsmith API's domain:
Expand Down Expand Up @@ -66,12 +69,11 @@ Flagsmith also maps user attributes from the following claims in the SAML assert

| Flagsmith attribute | IdP claims |
| ------------------- | ---------------------------------------------------- |
| `email` | `mail`, `email` or `emailAddress` |
| `first_name` | `gn`, `givenName` or the first part of `displayName` |
| `last_name` | `sn`, `surname` or the second part of `displayName` |
| Email | `mail`, `email` or `emailAddress` |
| First name | `gn`, `givenName` or the first part of `displayName` |
| Last name | `sn`, `surname` or the second part of `displayName` |

You can override these mappings by adding the corresponding IdP attribute names to your SAML configuration from the
Django admin interface.
To add custom attribute mappings, edit your SAML configuration and open the Attribute Mappings tab.

## Permissions for SAML users

Expand Down
4 changes: 2 additions & 2 deletions frontend/web/components/SAMLAttributeMappingTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ const SAMLAttributeMappingTable: FC<SAMLAttributeMappingTableType> = ({
header={
<Row className='table-header'>
<Flex className='table-column px-3'>
<div className='font-weight-medium'>SAML Attribute Name</div>
<div className='font-weight-medium'>SAML attribute name</div>
</Flex>
<Flex className='table-column px-3'>
<div className='table-column' style={{ width: '375px' }}>
IDP Attribute Name
IdP attribute name
</div>
</Flex>
</Row>
Expand Down
30 changes: 20 additions & 10 deletions frontend/web/components/SamlTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import CreateSAML from './modals/CreateSAML'
import Switch from './Switch'
import { SAMLConfiguration } from 'common/types/responses'
import PlanBasedBanner from './PlanBasedAccess'

export type SamlTabType = {
organisationId: number
}

const SamlTab: FC<SamlTabType> = ({ organisationId }) => {
const { data } = useGetSamlConfigurationsQuery({
organisation_id: organisationId,
Expand All @@ -37,15 +38,15 @@ const SamlTab: FC<SamlTabType> = ({ organisationId }) => {
return (
<PlanBasedBanner feature={'SAML'} theme={'page'} className='mt-3'>
<PageTitle
title={'SAML Configuration'}
title={'SAML Configurations'}
cta={
<Button
className='text-right'
onClick={() => {
openCreateSAML('Create SAML configuration', organisationId)
}}
>
{'Create a SAML Configuration'}
{'Create a SAML configuration'}
</Button>
}
/>
Expand All @@ -62,11 +63,17 @@ const SamlTab: FC<SamlTabType> = ({ organisationId }) => {
}
header={
<Row className='table-header'>
<Flex className='table-column px-3'>
<div className='font-weight-medium'>SAML Name</div>
<Flex className='table-column'>
<div className='font-weight-medium'>Configuration name</div>
</Flex>
<div className='table-column' style={{ width: '205px' }}>
Allow IDP Initiated
<div
className='table-column d-none d-md-block'
style={{ width: '150px' }}
>
Allow IdP-initiated
</div>
<div style={{ width: 90 }} className='table-column'>
Action
</div>
</Row>
}
Expand All @@ -81,16 +88,19 @@ const SamlTab: FC<SamlTabType> = ({ organisationId }) => {
)
}}
space
className='list-item clickable cursor-pointer'
className='list-item py-2 py-md-0 clickable cursor-pointer'
key={samlConf.name}
>
<Flex className='table-column px-3'>
<div className='font-weight-medium mb-1'>{samlConf.name}</div>
</Flex>
<div className='table-column' style={{ width: '95px' }}>
<div
className='table-column d-none d-md-flex gap-4 align-items-center'
style={{ width: '150px' }}
>
<Switch checked={samlConf.allow_idp_initiated} />
</div>
<div className='table-column'>
<div className='table-column' style={{ width: 90 }}>
<Button
id='delete-invite'
type='button'
Expand Down
59 changes: 48 additions & 11 deletions frontend/web/components/modals/CreateSAML.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import Tabs from 'components/base/forms/Tabs'
import TabItem from 'components/base/forms/TabItem'
import { AttributeName } from 'common/types/responses'
import SAMLAttributeMappingTable from 'components/SAMLAttributeMappingTable'
import Input from 'components/base/forms/Input'
import Icon from 'components/Icon'
import Project from 'common/project'

type CreateSAML = {
organisationId: number
Expand Down Expand Up @@ -52,6 +55,12 @@ const CreateSAML: FC<CreateSAML> = ({ organisationId, samlName }) => {
{ skip: !samlName },
)

const acsUrl = new URL(`/auth/saml/${name}/response/`, Project.api).href
const copyAcsUrl = async () => {
await navigator.clipboard.writeText(acsUrl)
toast('Copied to clipboard')
}

useEffect(() => {
if (isSuccess && data) {
setPreviousName(data.name)
Expand Down Expand Up @@ -101,9 +110,10 @@ const CreateSAML: FC<CreateSAML> = ({ organisationId, samlName }) => {
className='mt-2'
title='Name*'
data-test='saml-name'
tooltip='A short name for the organization, used as the input when clicking "Single Sign-on" at login, should only consist of alphanumeric characters, plus (+), underscore (_), and hyphen (-).'
tooltip='An URL-friendly name for this configuration, used as the input when selecting "Single Sign-On" at login. It determines the Assertion Consumer Service (ACS) URL that your identity provider must post SAML responses to. This cannot be changed after the SAML configuration is created.'
tooltipPlace='right'
value={name}
disabled={isEdit}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const newName = Utils.safeParseEventValue(event).replace(/ /g, '_')
if (validateName(newName)) {
Expand All @@ -121,7 +131,7 @@ const CreateSAML: FC<CreateSAML> = ({ organisationId, samlName }) => {
className='mt-2 mb-4'
title='Frontend URL*'
data-test='frontend-url'
tooltip='The base URL of the Flagsmith dashboard'
tooltip='The base URL of the Flagsmith dashboard. Users will be redirected here after authenticating successfully.'
tooltipPlace='right'
value={data?.frontend_url || frontendUrl}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -136,8 +146,8 @@ const CreateSAML: FC<CreateSAML> = ({ organisationId, samlName }) => {
/>
<InputGroup
className='mt-2 mb-4'
title='Allow IDP initiated'
tooltip='Determines whether logins can be initiated from the IDP'
title='Allow IdP-initiated logins'
tooltip="Enable this to allow logins initiated by your identity provider. If disabled, users can only log in from Flagsmith's login page."
tooltipPlace='right'
component={
<Switch
Expand All @@ -151,7 +161,7 @@ const CreateSAML: FC<CreateSAML> = ({ organisationId, samlName }) => {
<FormGroup className='mb-1'>
<div className='mt-2 p-0'>
<Row>
<label className='form-label'>IDP Metadata XML</label>
<label className='form-label'>IdP metadata XML</label>
{data?.idp_metadata_xml && (
<div className='ml-2 clickable' onClick={downloadIDPMetadata}>
<Tooltip
Expand Down Expand Up @@ -203,6 +213,35 @@ const CreateSAML: FC<CreateSAML> = ({ organisationId, samlName }) => {
<ErrorMessage error={createError || updateError} />
</div>
)}
{isEdit && (
<div className='mt-12'>
<Tooltip
title={
<label>
Assertion Consumer Service (ACS) URL
<Icon name='info-outlined' />
</label>
}
>
Also known as sign-on URL. Your identity provider needs to know this
URL to send SAML responses to it.
</Tooltip>
<div
onClick={(e) => e.stopPropagation()}
className='flex flex-row gap-2'
>
<Input className='w-full flex-1' value={acsUrl} readOnly />
<Button
onClick={() => {
copyAcsUrl()
}}
className='me-2 btn-with-icon'
>
<Icon name='copy' width={20} fill='#656D7B' />
</Button>
</div>
</div>
)}
<div className='text-right py-2'>
{isEdit && (
<Button
Expand Down Expand Up @@ -273,13 +312,11 @@ const CreateSAML: FC<CreateSAML> = ({ organisationId, samlName }) => {
return (
<div className='create-feature-tab mt-3'>
<InputGroup
title={'SAML Attribute Name*'}
tooltip='This is the attribute name where you want to store the information received from the SAML identity provider'
tooltipPlace='right'
title={'Flagsmith user attribute'}
component={
<Select
value={djangoAttributeName}
placeholder='Select a SAML attribute name'
placeholder='Select a Flagsmith user attribute'
options={samlAttributes}
onChange={(m: samlAttributeType) => {
setDjangoAttributeName(m)
Expand All @@ -290,9 +327,9 @@ const CreateSAML: FC<CreateSAML> = ({ organisationId, samlName }) => {
/>
<InputGroup
className='mt-2'
title='IDP Attribute Name*'
title='IdP attribute name*'
data-test='attribute-name'
tooltip='This is the specific value of the attribute sent by the SAML identity provider'
tooltip='The value(s) of this SAML attribute from your identity provider will be saved to the selected Flagsmith user attribute.'
tooltipPlace='right'
value={ipdAttributeName}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
Expand Down

0 comments on commit 3f561ee

Please sign in to comment.