Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SAML2 group mapping #11273

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open

Conversation

fopina
Copy link

@fopina fopina commented Nov 16, 2024

Description

While SAML2 authentication works well as it is, it's not very useful for authorization.

This PR introduces the ability to map SAML2 IdP groups (or some other list attribute) to Dojo groups.
And its configuration is similar to Azure AD group matching:

  • Take all groups from IdP that match a configurable regexp
  • Take all groups from the user on Dojo that match the same regexp
  • Take all existing groups in Dojo that match the same regexp

Testing

Auth0 was used to test the setup as it support SAML2

  • Create new application, Regular Web Application
  • Under application addons, enable SAML2 web-app
  • Use settings
{
 "mappings": {
   "user_id":     "urn:oid:0.9.2342.19200300.100.1.1",
   "email":       "urn:oid:1.2.840.113549.1.9.1.1",
   "given_name":  "urn:oid:2.5.4.3",
   "family_name": "urn:oid:2.5.4.4",
   "groups":      "http://schemas.xmlsoap.org/claims/Group"
 },
 "passthroughClaimsWithNoMapping": false
}
  • Add http://localhost:8080/saml/acs/ as callback url and click Enable

As there are no groups in Auth0 and roles are not shared via SAML assertions, we can push them (roles) into a custom assertion (based on this and this links

  • Go to Actions choose Trigger
  • Choose postlogin trigger
/**
* Handler that will be called during the execution of a PostLogin flow.
*
* @param {Event} event - Details about the user and the context in which they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
  var roles = event.authorization.roles;
	api.samlResponse.setAttribute('urn:oid:2.5.4.33', roles);
};
  • Back to Dojo, set the following extra configuration (via env):
DD_SAML2_ENABLED: 'true'
DD_SAML2_METADATA_AUTO_CONF_URL: <METADATA URL FROM AUTH0 SAML2 ADDON>
DD_SITE_URL: http://localhost:8080
DD_SOCIAL_LOGIN_AUTO_REDIRECT: "true"
DD_SOCIAL_AUTH_SHOW_LOGIN_FORM: "false"
DD_SAML2_CREATE_USER: "true"
DD_SAML2_ATTRIBUTES_MAP: email=email,uid=username,cn=first_name,sn=last_name
DD_SAML2_GROUPS_FILTER='.*'
DD_SAML2_GROUPS_ATTRIBUTE=roleOccupant
  • Now create some roles and add them to the test user in Auth0.
  • Create Dojo groups with the exame same name
  • Login using that test user and the user should be part of the mapped Dojo groups
    • You can verify in logs that those groups were deteted

@github-actions github-actions bot added docker settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR apiv2 docs unittests integration_tests ui parser helm labels Nov 16, 2024
Copy link

dryrunsecurity bot commented Nov 16, 2024

DryRun Security Summary

The provided code changes focus on updating the SAML2 authentication configuration and implementing a custom SAML2 authentication backend for the DefectDojo application, including the ability to map SAML2 groups to DefectDojo groups and introducing new settings to allow for more flexible group mapping.

Expand for full summary

Summary:

The provided code changes focus on updating the SAML2 authentication configuration and implementing a custom SAML2 authentication backend for the DefectDojo application. The key changes include:

  1. Updating the SAML2 authentication settings to use a custom Saml2Backend class, which allows for more granular control over the SAML2 authentication process, including the ability to map SAML2 groups to DefectDojo groups.
  2. Introducing two new settings, DD_SAML2_GROUPS_FILTER and DD_SAML2_GROUPS_ATTRIBUTE, to allow for more flexible group mapping between SAML2 and DefectDojo.
  3. Implementing a custom Saml2Backend class that overrides the _update_user() method to handle the mapping of SAML2 groups to DefectDojo groups, ensuring that the user's group membership is kept in sync.

From an application security perspective, these changes do not introduce any obvious security concerns. The SAML2 authentication process is a common and well-established method for integrating single sign-on functionality, and the specific changes made here are focused on improving the user experience and administration of the SAML2 integration. However, it's important to ensure that the SAML2 configuration is properly secured, with appropriate validation of SAML assertions and the use of secure protocols (e.g., HTTPS) to prevent potential security vulnerabilities. Additionally, regular review of the SAML2 integration and monitoring for changes in the SAML2 provider's security practices or vulnerabilities is recommended.

Files Changed:

  1. dojo/settings/.settings.dist.py.sha256sum: This file contains the SHA-256 checksum of the .settings.dist.py file, which is likely a configuration file that contains sensitive information. The code change updates the checksum value, indicating that the contents of the .settings.dist.py file have been modified. It's important to ensure that the new checksum value matches the actual contents of the .settings.dist.py file and that the changes do not introduce any security vulnerabilities or expose sensitive information.
  2. dojo/settings/settings.dist.py: This file contains the SAML2 authentication configuration for the DefectDojo application. The code changes update the DD_SAML2_AUTHENTICATION_BACKENDS setting to use a custom dojo.backends.Saml2Backend and introduce two new settings, DD_SAML2_GROUPS_FILTER and DD_SAML2_GROUPS_ATTRIBUTE, to allow for more granular control over the SAML2 group mapping process.
  3. dojo/backends.py: This file contains the implementation of the custom Saml2Backend class, which inherits from the base Saml2Backend provided by the djangosaml2 library. The primary focus of this code change is to handle the mapping of SAML2 groups to DefectDojo/Django groups, ensuring that the user's group membership is kept in sync.

Code Analysis

We ran 9 analyzers against 3 files and 1 analyzer had findings. 8 analyzers had no findings.

Analyzer Findings
Authn/Authz Analyzer 2 findings

Riskiness

🟢 Risk threshold not exceeded.

View PR in the DryRun Dashboard.

@fopina fopina changed the base branch from master to dev November 16, 2024 01:22
Copy link
Contributor

@kiblik kiblik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this change. I was considering similar functionality in the past, and I'm happy that somebody has done it now.

A couple of comments:

  • Thanks for the nice explanation in the description of PR, can you put it in the documentation as well? https://documentation.defectdojo.com/integrations/social-authentication/
  • If you have an implementation that can filter-out the right list of groups, why not create them? I'm not saying it has to be default, but opt-in.
  • Other social provider's groups are markered (Remote, Azure). Can you add ”SAML” as a social provider?
    social_provider = models.CharField(max_length=10, choices=SOCIAL_CHOICES, blank=True, null=True, help_text=_("Group imported from a social provider."), verbose_name=_("Social Authentication Provider"))

Thank you

dojo/backends.py Outdated Show resolved Hide resolved
return user

# list of all existing "SAML2-mapped" groups
all_saml_groups = {group.name: group for group in Dojo_Group.objects.all() if self.group_re.match(group.name)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
all_saml_groups = {group.name: group for group in Dojo_Group.objects.all() if self.group_re.match(group.name)}
all_saml_groups = {group.name: group for group in Dojo_Group.objects.filter(social_provider=Dojo_Group.SAML) if self.group_re.match(group.name)}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this makes sense thinking of the AzureAD pipeline yet it "forces" groups to have been created by this (instead of added manually to match IdP groups), right?

@@ -177,6 +177,11 @@
"Lastname": "last_name",
}),
DD_SAML2_ALLOW_UNKNOWN_ATTRIBUTE=(bool, False),
# similar to DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GROUPS_FILTER, regular expression for which SAML2 groups to map to Dojo groups (with same name)
# Groups need to already exist in Dojo. And if value is not set, no group processing is done
DD_SAML2_GROUPS_FILTER=(str, ""),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you call it _READERS as it adds a Reader role? In the future

Suggested change
DD_SAML2_GROUPS_FILTER=(str, ""),
DD_SAML2_GROUPS_FILTER_READERS=(str, ""),

Or even more future-proof (if new roles would be created): dict with mapping to roles

Suggested change
DD_SAML2_GROUPS_FILTER=(str, ""),
DD_SAML2_GROUPS_FILTER=(dict, {"readers": ""}),

More info, about how dict works with environ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first sight, I agree this would make sense, but does it really? When would anyone not be just reader in a group where the membership is expected to be managed by IdP?

Noone should be able to add/remove people from the group in Dojo, they should do it in IdP, otherwise that person will eventually be removed from the group (upon login)

Maybe I'm missing some use case in my mind, please let me know if so. Otherwise this would just increase complexity of the configuration without any added benefit

@fopina
Copy link
Author

fopina commented Nov 16, 2024

Thank you @kiblik

Quick feedback on number 2: my initial implementation was to create the groups on demand (not optional) but there's the save signal on groups that uses user for that request and fails (with NoneType exception as it is unexpected) because there's no user (at that time yet as not done authenticating).

I decided to remove creation as that also allows more granular control on the groups.

If I make it optional too, it would allow both things, but what's the right fix on the owner? Should I modify the signal too to allow a group without owner?

@kiblik
Copy link
Contributor

kiblik commented Nov 16, 2024

If I make it optional too, it would allow both things, but what's the right fix on the owner? Should I modify the signal too to allow a group without owner?

Is it the owner of a group? I'm not able to find this attribute. Can you send the link to which signal was failing, please?

@fopina
Copy link
Author

fopina commented Nov 16, 2024

I believe it was this one https://github.com/fopina/django-DefectDojo/blob/268583954fb74635fc708c49d8826e1534cd8f5d/dojo/group/utils.py#L37
Not 100% sure as I’ve been using this in my fork for a few months now

Easy to fix in several ways, not sure which one is preferred

@kiblik
Copy link
Contributor

kiblik commented Nov 16, 2024

Easy to fix in several ways, not sure which one is preferred

It is hard to say about preference but impersonate came to my mind first.

@fopina
Copy link
Author

fopina commented Nov 16, 2024

I meant less technically but more business oriented: which user should own the group? Should anyone?

This was the reason I didn’t try to solve it.
And I have the impression it’s the same for AzureAD, groups are not created (not confirmed though as I’m mobile now)

@fopina
Copy link
Author

fopina commented Nov 16, 2024

@kiblik my bad, AzureAD pipeline does create the groups but one of the required settings skips that owner user in the signal.

I guess I can simply do the same then, I'll update the PR next week 👍

Copy link
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

Copy link
Contributor

Conflicts have been resolved. A maintainer will review the pull request shortly.

Copy link
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
conflicts-detected settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants