-
Notifications
You must be signed in to change notification settings - Fork 429
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add UI for configuring SAML in Flagsmith (#4055)
Co-authored-by: Matthew Elwell <[email protected]>
- Loading branch information
1 parent
245ad60
commit d2c2aba
Showing
11 changed files
with
766 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,45 +8,24 @@ SAML authentication requires an [Enterprise subscription](https://flagsmith.com/ | |
|
||
::: | ||
|
||
## Setup (SaaS) | ||
## Setup | ||
|
||
To enable SAML authentication for your Flagsmith organisation, you must send your identity provider metadata XML | ||
document to [[email protected]](mailto:[email protected]). | ||
To enable SAML authentication for your Flagsmith organisation, you have to go to your organisations settings, and in the | ||
SAML tab, you'll be able to configure it. | ||
|
||
Once Flagsmith has configured your identity provider, we will send you a service provider metadata XML document or an | ||
Assertion Consumer Service (ACS) URL to use with your identity provider. | ||
In the UI, you will be able to configure the following fields. | ||
|
||
## Setup (self-hosted) | ||
**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 + '-,\_'). | ||
|
||
To enable SAML for your Flagsmith organisation in a self-hosted environment, you will need access the | ||
[Django admin interface](/deployment/configuration/django-admin). | ||
**Frontend URL**: (**Required**) This should be the base URL of the Flagsmith dashboard. | ||
|
||
In the Django admin interface, click on the "SAML Configurations" option in the menu on the left. To create a new SAML | ||
configuration, click on "Add SAML Configuration" in the top right corner. | ||
**Allow IdP initiated**: This field determines whether logins can be initiated from the IdP. | ||
|
||
You should see a screen similar to the following: | ||
**IdP metadata xml**: The metadata from the IdP. | ||
|
||
 | ||
|
||
From the drop down next to **Organisation**, select the organisation that you want to configure for SAML authentication. | ||
|
||
Next to **Organisation name**, add a URI-safe name that uniquely identifies the organisation. Users will need to provide | ||
this name when selecting the "Single Sign-On" option at the Flagsmith login screen. | ||
|
||
Next to **Frontend URL**, add the URL where your Flagsmith frontend is running. Users will be redirected to this URL | ||
when they authenticate using SAML. | ||
|
||
Copy your identity provider's XML metadata document into the **IdP metadata XML** field, or leave it blank and come back | ||
to this step later if you do not have it. | ||
|
||
If you want to enable IdP-initiated SSO, check the box next to **Allow IdP-initiated (unsolicited) login**. If you are | ||
unsure, leave this box unchecked. | ||
|
||
Hit the **Save** button to create the SAML configuration. | ||
|
||
Once your SAML configuration is created, you can download your Flagsmith service provider metadata by going back to the | ||
list of SAML configurations in the Django admin interface and clicking "Download" on the SAML configuration you just | ||
created. | ||
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 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
import { Res } from 'common/types/responses' | ||
import { Req } from 'common/types/requests' | ||
import { service } from 'common/service' | ||
import Utils from 'common/utils/utils' | ||
|
||
export const samlConfigurationService = service | ||
.enhanceEndpoints({ | ||
addTagTypes: ['SamlConfiguration', 'samlConfigurations'], | ||
}) | ||
.injectEndpoints({ | ||
endpoints: (builder) => ({ | ||
createSamlConfiguration: builder.mutation< | ||
Res['samlConfiguration'], | ||
Req['createSamlConfiguration'] | ||
>({ | ||
invalidatesTags: [ | ||
{ id: 'LIST', type: 'SamlConfiguration' }, | ||
{ id: 'LIST', type: 'samlConfigurations' }, | ||
], | ||
query: (query: Req['createSamlConfiguration']) => ({ | ||
body: query, | ||
method: 'POST', | ||
url: `auth/saml/configuration/`, | ||
}), | ||
}), | ||
deleteSamlConfiguration: builder.mutation< | ||
Res['samlConfiguration'], | ||
Req['deleteSamlConfiguration'] | ||
>({ | ||
invalidatesTags: [ | ||
{ id: 'LIST', type: 'SamlConfiguration' }, | ||
{ id: 'LIST', type: 'samlConfigurations' }, | ||
], | ||
query: (query: Req['deleteSamlConfiguration']) => ({ | ||
body: query, | ||
method: 'DELETE', | ||
url: `auth/saml/configuration/${query.name}/`, | ||
}), | ||
}), | ||
getSamlConfiguration: builder.query< | ||
Res['samlConfiguration'], | ||
Req['getSamlConfiguration'] | ||
>({ | ||
providesTags: (res) => [{ id: res?.name, type: 'SamlConfiguration' }], | ||
query: (query: Req['getSamlConfiguration']) => ({ | ||
url: `auth/saml/configuration/${query.name}/`, | ||
}), | ||
}), | ||
getSamlConfigurationMetadata: builder.query< | ||
Res['samlMetadata'], | ||
Req['getSamlConfigurationMetadata'] | ||
>({ | ||
providesTags: (res) => [ | ||
{ id: res?.entity_id, type: 'SamlConfiguration' }, | ||
], | ||
query: (query: Req['getSamlConfigurationMetadata']) => ({ | ||
headers: { Accept: 'application/xml' }, | ||
url: `auth/saml/${query.name}/metadata/`, | ||
}), | ||
}), | ||
getSamlConfigurations: builder.query< | ||
Res['samlConfigurations'], | ||
Req['getSamlConfigurations'] | ||
>({ | ||
providesTags: [{ id: 'LIST', type: 'samlConfigurations' }], | ||
query: (query: Req['getSamlConfigurations']) => ({ | ||
url: `auth/saml/configuration/?${Utils.toParam({ | ||
organisation: query.organisation_id, | ||
})}`, | ||
}), | ||
}), | ||
updateSamlConfiguration: builder.mutation< | ||
Res['samlConfiguration'], | ||
Req['updateSamlConfiguration'] | ||
>({ | ||
invalidatesTags: (res) => [ | ||
{ id: 'LIST', type: 'SamlConfiguration' }, | ||
{ id: 'LIST', type: 'samlConfigurations' }, | ||
{ id: res?.name, type: 'SamlConfiguration' }, | ||
], | ||
query: (query: Req['updateSamlConfiguration']) => ({ | ||
body: query.body, | ||
method: 'PUT', | ||
url: `auth/saml/configuration/${query.name}/`, | ||
}), | ||
}), | ||
// END OF ENDPOINTS | ||
}), | ||
}) | ||
|
||
export async function createSamlConfiguration( | ||
store: any, | ||
data: Req['createSamlConfiguration'], | ||
options?: Parameters< | ||
typeof samlConfigurationService.endpoints.createSamlConfiguration.initiate | ||
>[1], | ||
) { | ||
return store.dispatch( | ||
samlConfigurationService.endpoints.createSamlConfiguration.initiate( | ||
data, | ||
options, | ||
), | ||
) | ||
} | ||
export async function deleteSamlConfiguration( | ||
store: any, | ||
data: Req['deleteSamlConfiguration'], | ||
options?: Parameters< | ||
typeof samlConfigurationService.endpoints.deleteSamlConfiguration.initiate | ||
>[1], | ||
) { | ||
return store.dispatch( | ||
samlConfigurationService.endpoints.deleteSamlConfiguration.initiate( | ||
data, | ||
options, | ||
), | ||
) | ||
} | ||
export async function getSamlConfiguration( | ||
store: any, | ||
data: Req['getSamlConfiguration'], | ||
options?: Parameters< | ||
typeof samlConfigurationService.endpoints.getSamlConfiguration.initiate | ||
>[1], | ||
) { | ||
return store.dispatch( | ||
samlConfigurationService.endpoints.getSamlConfiguration.initiate( | ||
data, | ||
options, | ||
), | ||
) | ||
} | ||
export async function getSamlConfigurations( | ||
store: any, | ||
data: Req['getSamlConfigurations'], | ||
options?: Parameters< | ||
typeof samlConfigurationService.endpoints.getSamlConfigurations.initiate | ||
>[1], | ||
) { | ||
return store.dispatch( | ||
samlConfigurationService.endpoints.getSamlConfigurations.initiate( | ||
data, | ||
options, | ||
), | ||
) | ||
} | ||
export async function getSamlConfigurationMetadata( | ||
store: any, | ||
data: Req['getSamlConfigurationMetadata'], | ||
options?: Parameters< | ||
typeof samlConfigurationService.endpoints.getSamlConfigurationMetadata.initiate | ||
>[1], | ||
) { | ||
return store.dispatch( | ||
samlConfigurationService.endpoints.getSamlConfigurationMetadata.initiate( | ||
data, | ||
options, | ||
), | ||
) | ||
} | ||
export async function updateSamlConfiguration( | ||
store: any, | ||
data: Req['updateSamlConfiguration'], | ||
options?: Parameters< | ||
typeof samlConfigurationService.endpoints.updateSamlConfiguration.initiate | ||
>[1], | ||
) { | ||
return store.dispatch( | ||
samlConfigurationService.endpoints.updateSamlConfiguration.initiate( | ||
data, | ||
options, | ||
), | ||
) | ||
} | ||
// END OF FUNCTION_EXPORTS | ||
|
||
export const { | ||
useCreateSamlConfigurationMutation, | ||
useDeleteSamlConfigurationMutation, | ||
useGetSamlConfigurationMetadataQuery, | ||
useGetSamlConfigurationQuery, | ||
useGetSamlConfigurationsQuery, | ||
useUpdateSamlConfigurationMutation, | ||
// END OF EXPORTS | ||
} = samlConfigurationService | ||
|
||
/* Usage examples: | ||
const { data, isLoading } = useGetSamlConfigurationQuery({ id: 2 }, {}) //get hook | ||
const [createSamlConfiguration, { isLoading, data, isSuccess }] = useCreateSamlConfigurationMutation() //create hook | ||
samlConfigurationService.endpoints.getSamlConfiguration.select({id: 2})(store.getState()) //access data from any function | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.