From 9b50cba4e79c45c6064c857fbef2eea437cc0e5c Mon Sep 17 00:00:00 2001 From: David Anderson Date: Tue, 23 Jul 2024 12:30:00 -0400 Subject: [PATCH] add login option using generic OIDC --- .../en/integrations/social-authentication.md | 71 ++++-- dojo/context_processors.py | 1 + dojo/settings/settings.dist.py | 203 ++++++++++++------ dojo/templates/dojo/login.html | 8 + dojo/user/views.py | 3 + 5 files changed, 197 insertions(+), 89 deletions(-) diff --git a/docs/content/en/integrations/social-authentication.md b/docs/content/en/integrations/social-authentication.md index ebf2a6b0c84..172439375ec 100644 --- a/docs/content/en/integrations/social-authentication.md +++ b/docs/content/en/integrations/social-authentication.md @@ -191,7 +191,7 @@ user, such as 'superuser'. button on the login page which should *magically* work ### Automatic Import of User-Groups -To import groups from Azure AD users, the following environment variable needs to be set: +To import groups from Azure AD users, the following environment variable needs to be set: {{< highlight python >}} DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GET_GROUPS=True @@ -200,16 +200,16 @@ To import groups from Azure AD users, the following environment variable needs t This will ensure the user is added to all the groups found in the Azure AD Token. Any missing groups will be created in DefectDojo (unless filtered). This group synchronization allows for product access via groups to limit the products a user can interact with. The Azure AD token returned by Azure will also need to be configured to include group IDs. Without this step, the -token will not contain any notion of a group, and the mapping process will report that the current user is not a member of any +token will not contain any notion of a group, and the mapping process will report that the current user is not a member of any groups. To update the the format of the token, add a group claim that applies to whatever group type you are using. -If unsure of what type that is, select `All Groups`. Do not activate `Emit groups as role claims` within the Azure AD +If unsure of what type that is, select `All Groups`. Do not activate `Emit groups as role claims` within the Azure AD "Token configuration" page. Application API permissions need to be updated with the `Group.Read.All` permission so that groups can be read on behalf of the user that has successfully signed in. To limit the amount of groups imported from Azure AD, a regular expression can be used as the following: - + {{< highlight python >}} DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GROUPS_FILTER='^team-.*' # or 'teamA|teamB|groupC' {{< /highlight >}} @@ -262,7 +262,7 @@ Follow along below. {{< /highlight >}} **Important:** if you enable this setting on already working instance with gitlab integrations, it will require new grant "read_repository" by user - + 5. Restart DefectDojo, and you should now see a **Login with Gitlab** button on the login page. @@ -270,7 +270,7 @@ Follow along below. There is also an option to use Keycloak as OAuth2 provider in order to authenticate users to Defect Dojo, also by using the social-auth plugin. -Here are suggestion on how to configure Keycloak and DefectDojo: +Here are suggestion on how to configure Keycloak and DefectDojo: ### Configure Keycloak (assuming you already have an existing realm, otherwise create one) @@ -283,7 +283,7 @@ Here are suggestion on how to configure Keycloak and DefectDojo: * Under `Fine grained openID connect configuration` -> `request object signature algorithm`: set to `RS256` * -> save these settings in keycloak (hit save button) 3. Under `Scope` -> `Full Scope Allowed` set to `off` -4. Under `mappers` -> add a custom mapper here: +4. Under `mappers` -> add a custom mapper here: * Name: `aud` * Mapper type: `audience` * Included audience: select your client/client-id here @@ -294,6 +294,35 @@ Here are suggestion on how to configure Keycloak and DefectDojo: 7. In your realm settings -> general -> endpoints: look into openId endpoint configuration and look up your authorization and token endpoint (use them below) +### Configure OIDC +Provides the option to authenticate users using a generic OIDC provider. + +The minimum configuration requires: + + {{< highlight python >}} + DD_SOCIAL_AUTH_OIDC_AUTH_ENABLED=True, + DD_SOCIAL_AUTH_OIDC_OIDC_ENDPOINT=(str, 'https://example.com'), + DD_SOCIAL_AUTH_OIDC_KEY=(str, 'YOUR_CLIENT_ID'), + DD_SOCIAL_AUTH_OIDC_SECRET=(str, 'YOUR_CLIENT_SECRET') + {{< /highlight >}} + +The rest of the OIDC configuration will be auto-detected by fetching data from: + - /.well-known/open-id-configuration/ + +You can also optionally set the following: + + {{< highlight python >}} + DD_SOCIAL_AUTH_OIDC_ID_KEY=(str, ''), #the key associated with the OIDC user IDs + DD_SOCIAL_AUTH_OIDC_USERNAME_KEY=(str, ''), #the key associated with the OIDC usernames + DD_SOCIAL_AUTH_OIDC_WHITELISTED_DOMAINS=(list, ['']), #list of domains allowed for login + DD_SOCIAL_AUTH_OIDC_JWT_ALGORITHMS=(list, ["RS256","HS256"]), + DD_SOCIAL_AUTH_OIDC_ID_TOKEN_ISSUER=(str, ''), + DD_SOCIAL_AUTH_OIDC_ACCESS_TOKEN_URL=(str, ''), + DD_SOCIAL_AUTH_OIDC_AUTHORIZATION_URL=(str, ''), + DD_SOCIAL_AUTH_OIDC_USERINFO_URL=(str, ''), + DD_SOCIAL_AUTH_OIDC_JWKS_URI=(str, ''), + {{< /highlight >}} + ### Configure Defect Dojo Edit the settings (see [Configuration]({{< ref "/getting_started/configuration" >}})) with the following information: @@ -304,13 +333,13 @@ Edit the settings (see [Configuration]({{< ref "/getting_started/configuration" DD_SECURE_SSL_REDIRECT=True, DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_ENABLED=True, DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY=(str, ''), - DD_SOCIAL_AUTH_KEYCLOAK_KEY=(str, ''), - DD_SOCIAL_AUTH_KEYCLOAK_SECRET=(str, ''), + DD_SOCIAL_AUTH_KEYCLOAK_KEY=(str, ''), + DD_SOCIAL_AUTH_KEYCLOAK_SECRET=(str, ''), DD_SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL=(str, ''), - DD_SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL=(str, '') + DD_SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL=(str, '') {{< /highlight >}} - -or, alternatively, for helm configuration, add this to the `extraConfig` section: + +or, alternatively, for helm configuration, add this to the `extraConfig` section: ```yaml DD_SESSION_COOKIE_SECURE: 'True' @@ -324,7 +353,7 @@ DD_SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL: '' DD_SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL: '' ``` -Optionally, you *can* set `DD_SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT` in order to customize the login button's text caption. +Optionally, you *can* set `DD_SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT` in order to customize the login button's text caption. ## GitHub Enterprise 1. Navigate to your GitHub Enterprise Server and follow instructions to create a new OAuth App [https://docs.github.com/en/enterprise-server/developers/apps/building-oauth-apps/creating-an-oauth-app](https://docs.github.com/en/enterprise-server/developers/apps/building-oauth-apps/creating-an-oauth-app) @@ -334,20 +363,20 @@ Optionally, you *can* set `DD_SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT` in order t - **https://the_hostname_you_have_dojo_deployed:your_server_port/complete/github-enterprise/** 4. Edit the settings (see [Configuration]({{< ref "/getting_started/configuration" >}})) with the following information: - {{< highlight python >}} - DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY=(str, 'GitHub Enterprise OAuth App Client ID'), - DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET=(str, 'GitHub Enterprise OAuth App Client Secret'), - DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_URL=(str, 'https://github..com/'), - DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL=(str, 'https://github..com/api/v3/'), - DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_OAUTH2_ENABLED = True, + {{< highlight python >}} + DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY=(str, 'GitHub Enterprise OAuth App Client ID'), + DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET=(str, 'GitHub Enterprise OAuth App Client Secret'), + DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_URL=(str, 'https://github..com/'), + DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL=(str, 'https://github..com/api/v3/'), + DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_OAUTH2_ENABLED = True, {{< /highlight >}} 5. Restart DefectDojo, and you should now see a **Login with GitHub Enterprise** - button on the login page. + button on the login page. ## SAML 2.0 In a similar direction to OAuth, this SAML addition provides a more secure perogative to SSO. For definitions of terms used and more information, -see the plugin [plugin homepage](https://github.com/IdentityPython/djangosaml2). +see the plugin [plugin homepage](https://github.com/IdentityPython/djangosaml2). 1. Navigate to your SAML IdP and find your metadata 2. Edit the settings (see [Configuration]({{< ref "/getting_started/configuration" >}})) with the following diff --git a/dojo/context_processors.py b/dojo/context_processors.py index 782cf767ce2..64d9f384784 100644 --- a/dojo/context_processors.py +++ b/dojo/context_processors.py @@ -9,6 +9,7 @@ def globalize_vars(request): "FORGOT_PASSWORD": settings.FORGOT_PASSWORD, "FORGOT_USERNAME": settings.FORGOT_USERNAME, "CLASSIC_AUTH_ENABLED": settings.CLASSIC_AUTH_ENABLED, + "OIDC_ENABLED": settings.OIDC_AUTH_ENABLED, "AUTH0_ENABLED": settings.AUTH0_OAUTH2_ENABLED, "GOOGLE_ENABLED": settings.GOOGLE_OAUTH_ENABLED, "OKTA_ENABLED": settings.OKTA_OAUTH_ENABLED, diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index df4ea452cb6..dadad83c0c8 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -110,6 +110,19 @@ DD_SOCIAL_AUTH_CREATE_USER=(bool, True), # if True creates user at first login DD_SOCIAL_LOGIN_AUTO_REDIRECT=(bool, False), # auto-redirect if there is only one social login method DD_SOCIAL_AUTH_TRAILING_SLASH=(bool, True), + DD_SOCIAL_AUTH_OIDC_AUTH_ENABLED=(bool, False), + DD_SOCIAL_AUTH_OIDC_OIDC_ENDPOINT=(str, ""), + DD_SOCIAL_AUTH_OIDC_ID_KEY=(str, ""), + DD_SOCIAL_AUTH_OIDC_KEY=(str, ""), + DD_SOCIAL_AUTH_OIDC_SECRET=(str, ""), + DD_SOCIAL_AUTH_OIDC_USERNAME_KEY=(str, ""), + DD_SOCIAL_AUTH_OIDC_WHITELISTED_DOMAINS=(list, [""]), + DD_SOCIAL_AUTH_OIDC_JWT_ALGORITHMS=(list, ["RS256", "HS256"]), + DD_SOCIAL_AUTH_OIDC_ID_TOKEN_ISSUER=(str, ""), + DD_SOCIAL_AUTH_OIDC_ACCESS_TOKEN_URL=(str, ""), + DD_SOCIAL_AUTH_OIDC_AUTHORIZATION_URL=(str, ""), + DD_SOCIAL_AUTH_OIDC_USERINFO_URL=(str, ""), + DD_SOCIAL_AUTH_OIDC_JWKS_URI=(str, ""), DD_SOCIAL_AUTH_AUTH0_OAUTH2_ENABLED=(bool, False), DD_SOCIAL_AUTH_AUTH0_KEY=(str, ""), DD_SOCIAL_AUTH_AUTH0_SECRET=(str, ""), @@ -168,14 +181,17 @@ DD_SAML2_ENTITY_ID=(str, ""), # Allow to create user that are not already in the Django database DD_SAML2_CREATE_USER=(bool, False), - DD_SAML2_ATTRIBUTES_MAP=(dict, { - # Change Email/UserName/FirstName/LastName to corresponding SAML2 userprofile attributes. - # format: SAML attrib:django_user_model - "Email": "email", - "UserName": "username", - "Firstname": "first_name", - "Lastname": "last_name", - }), + DD_SAML2_ATTRIBUTES_MAP=( + dict, + { + # Change Email/UserName/FirstName/LastName to corresponding SAML2 userprofile attributes. + # format: SAML attrib:django_user_model + "Email": "email", + "UserName": "username", + "Firstname": "first_name", + "Lastname": "last_name", + }, + ), DD_SAML2_ALLOW_UNKNOWN_ATTRIBUTE=(bool, False), # Authentication via HTTP Proxy which put username to HTTP Header REMOTE_USER DD_AUTH_REMOTEUSER_ENABLED=(bool, False), @@ -458,9 +474,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) -FILE_UPLOAD_HANDLERS = ( - "django.core.files.uploadhandler.TemporaryFileUploadHandler", -) +FILE_UPLOAD_HANDLERS = ("django.core.files.uploadhandler.TemporaryFileUploadHandler",) DATA_UPLOAD_MAX_MEMORY_SIZE = env("DD_DATA_UPLOAD_MAX_MEMORY_SIZE") @@ -490,6 +504,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param # These are the individidual modules supported by social-auth AUTHENTICATION_BACKENDS = ( + "social_core.backends.open_id_connect.OpenIdConnectAuth", "social_core.backends.auth0.Auth0OAuth2", "social_core.backends.google.GoogleOAuth2", "social_core.backends.okta.OktaOAuth2", @@ -582,6 +597,20 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param if GITLAB_PROJECT_AUTO_IMPORT: SOCIAL_AUTH_GITLAB_SCOPE += ["read_repository"] +OIDC_AUTH_ENABLED = env("DD_SOCIAL_AUTH_OIDC_AUTH_ENABLED") +SOCIAL_AUTH_OIDC_OIDC_ENDPOINT = env("DD_SOCIAL_AUTH_OIDC_OIDC_ENDPOINT") +SOCIAL_AUTH_OIDC_ID_KEY = env("DD_SOCIAL_AUTH_OIDC_ID_KEY") +SOCIAL_AUTH_OIDC_KEY = env("DD_SOCIAL_AUTH_OIDC_KEY") +SOCIAL_AUTH_OIDC_SECRET = env("DD_SOCIAL_AUTH_OIDC_SECRET") +SOCIAL_AUTH_OIDC_USERNAME_KEY = env("DD_SOCIAL_AUTH_OIDC_USERNAME_KEY") +SOCIAL_AUTH_OIDC_WHITELISTED_DOMAINS = env("DD_SOCIAL_AUTH_OIDC_WHITELISTED_DOMAINS") +SOCIAL_AUTH_OIDC_JWT_ALGORITHMS = env("DD_SOCIAL_AUTH_OIDC_JWT_ALGORITHMS") +SOCIAL_AUTH_OIDC_ID_TOKEN_ISSUER = env("DD_SOCIAL_AUTH_OIDC_ID_TOKEN_ISSUER") +SOCIAL_AUTH_OIDC_ACCESS_TOKEN_URL = env("DD_SOCIAL_AUTH_OIDC_ACCESS_TOKEN_URL") +SOCIAL_AUTH_OIDC_AUTHORIZATION_URL = env("DD_SOCIAL_AUTH_OIDC_AUTHORIZATION_URL") +SOCIAL_AUTH_OIDC_USERINFO_URL = env("DD_SOCIAL_AUTH_OIDC_USERINFO_URL") +SOCIAL_AUTH_OIDC_JWKS_URI = env("DD_SOCIAL_AUTH_OIDC_JWKS_URI") + AUTH0_OAUTH2_ENABLED = env("DD_SOCIAL_AUTH_AUTH0_OAUTH2_ENABLED") SOCIAL_AUTH_AUTH0_KEY = env("DD_SOCIAL_AUTH_AUTH0_KEY") SOCIAL_AUTH_AUTH0_SECRET = env("DD_SOCIAL_AUTH_AUTH0_SECRET") @@ -613,9 +642,15 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param SLA_NOTIFY_ACTIVE = env("DD_SLA_NOTIFY_ACTIVE") # this will include 'verified' findings as well as non-verified. SLA_NOTIFY_ACTIVE_VERIFIED_ONLY = env("DD_SLA_NOTIFY_ACTIVE_VERIFIED_ONLY") SLA_NOTIFY_WITH_JIRA_ONLY = env("DD_SLA_NOTIFY_WITH_JIRA_ONLY") # Based on the 2 above, but only with a JIRA link -SLA_NOTIFY_PRE_BREACH = env("DD_SLA_NOTIFY_PRE_BREACH") # in days, notify between dayofbreach minus this number until dayofbreach -SLA_NOTIFY_POST_BREACH = env("DD_SLA_NOTIFY_POST_BREACH") # in days, skip notifications for findings that go past dayofbreach plus this number -SLA_BUSINESS_DAYS = env("DD_SLA_BUSINESS_DAYS") # Use business days to calculate SLA's and age of a finding instead of calendar days +SLA_NOTIFY_PRE_BREACH = env( + "DD_SLA_NOTIFY_PRE_BREACH", +) # in days, notify between dayofbreach minus this number until dayofbreach +SLA_NOTIFY_POST_BREACH = env( + "DD_SLA_NOTIFY_POST_BREACH", +) # in days, skip notifications for findings that go past dayofbreach plus this number +SLA_BUSINESS_DAYS = env( + "DD_SLA_BUSINESS_DAYS", +) # Use business days to calculate SLA's and age of a finding instead of calendar days SEARCH_MAX_RESULTS = env("DD_SEARCH_MAX_RESULTS") @@ -634,6 +669,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param rf"^{URL_PREFIX}api/v2/", r"complete/", r"empty_questionnaire/([\d]+)/answer", + r"oauth2/idpresponse", rf"^{URL_PREFIX}password_reset/", rf"^{URL_PREFIX}forgot_username", rf"^{URL_PREFIX}reset/", @@ -665,9 +701,13 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param # https://django-ratelimit.readthedocs.io/en/stable/index.html RATE_LIMITER_ENABLED = env("DD_RATE_LIMITER_ENABLED") -RATE_LIMITER_RATE = env("DD_RATE_LIMITER_RATE") # Examples include 5/m 100/h and more https://django-ratelimit.readthedocs.io/en/stable/rates.html#simple-rates +RATE_LIMITER_RATE = env( + "DD_RATE_LIMITER_RATE", +) # Examples include 5/m 100/h and more https://django-ratelimit.readthedocs.io/en/stable/rates.html#simple-rates RATE_LIMITER_BLOCK = env("DD_RATE_LIMITER_BLOCK") # Block the requests after rate limit is exceeded -RATE_LIMITER_ACCOUNT_LOCKOUT = env("DD_RATE_LIMITER_ACCOUNT_LOCKOUT") # Forces the user to change password on next login. +RATE_LIMITER_ACCOUNT_LOCKOUT = env( + "DD_RATE_LIMITER_ACCOUNT_LOCKOUT", +) # Forces the user to change password on next login. # ------------------------------------------------------------------------------ # SECURITY DIRECTIVES @@ -709,7 +749,9 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param # Unless set to None, the SecurityMiddleware sets the Cross-Origin Opener Policy # header on all responses that do not already have it to the value provided. -SECURE_CROSS_ORIGIN_OPENER_POLICY = env("DD_SECURE_CROSS_ORIGIN_OPENER_POLICY") if env("DD_SECURE_CROSS_ORIGIN_OPENER_POLICY") != "None" else None +SECURE_CROSS_ORIGIN_OPENER_POLICY = ( + env("DD_SECURE_CROSS_ORIGIN_OPENER_POLICY") if env("DD_SECURE_CROSS_ORIGIN_OPENER_POLICY") != "None" else None +) if env("DD_SECURE_PROXY_SSL_HEADER"): SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") @@ -765,12 +807,8 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.BasicAuthentication", ), - "DEFAULT_PERMISSION_CLASSES": ( - "rest_framework.permissions.DjangoModelPermissions", - ), - "DEFAULT_RENDERER_CLASSES": ( - "rest_framework.renderers.JSONRenderer", - ), + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.DjangoModelPermissions",), + "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 25, "EXCEPTION_HANDLER": "dojo.api_v2.exception_handler.custom_exception_handler", @@ -892,8 +930,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param ] MIDDLEWARE = MIDDLEWARE + WHITE_NOISE -EMAIL_CONFIG = env.email_url( - "DD_EMAIL_URL", default="smtp://user@:password@localhost:25") +EMAIL_CONFIG = env.email_url("DD_EMAIL_URL", default="smtp://user@:password@localhost:25") vars().update(EMAIL_CONFIG) @@ -922,6 +959,7 @@ def saml2_attrib_map_format(dict): import saml2 import saml2.saml + # SSO_URL = env('DD_SSO_URL') SAML_METADATA = {} if len(env("DD_SAML2_METADATA_AUTO_CONF_URL")) > 0: @@ -935,7 +973,7 @@ def saml2_attrib_map_format(dict): SAML_LOGOUT_REQUEST_PREFERRED_BINDING = saml2.BINDING_HTTP_POST SAML_IGNORE_LOGOUT_ERRORS = True SAML_DJANGO_USER_MAIN_ATTRIBUTE = "username" -# SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP = '__iexact' + # SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP = '__iexact' SAML_USE_NAME_ID_AS_USERNAME = True SAML_CREATE_UNKNOWN_USER = env("DD_SAML2_CREATE_USER") SAML_ATTRIBUTE_MAPPING = saml2_attrib_map_format(env("DD_SAML2_ATTRIBUTES_MAP")) @@ -950,10 +988,8 @@ def saml2_attrib_map_format(dict): SAML_CONFIG = { # full path to the xmlsec1 binary programm "xmlsec_binary": "/usr/bin/xmlsec1", - # your entity id, usually your subdomain plus the url to the metadata view "entityid": str(SAML2_ENTITY_ID), - # directory with attribute mapping "attribute_map_dir": path.join(BASEDIR, "attribute-maps"), # do now discard attributes not specified in attribute-maps @@ -968,41 +1004,32 @@ def saml2_attrib_map_format(dict): "want_assertions_signed": True, "force_authn": SAML_FORCE_AUTH, "allow_unsolicited": True, - # For Okta add signed logout requets. Enable this: # "logout_requests_signed": True, - "endpoints": { # url and binding to the assetion consumer service view # do not change the binding or service name "assertion_consumer_service": [ - (f"{SITE_URL}/saml2/acs/", - saml2.BINDING_HTTP_POST), + (f"{SITE_URL}/saml2/acs/", saml2.BINDING_HTTP_POST), ], # url and binding to the single logout service view # do not change the binding or service name "single_logout_service": [ # Disable next two lines for HTTP_REDIRECT for IDP's that only support HTTP_POST. Ex. Okta: - (f"{SITE_URL}/saml2/ls/", - saml2.BINDING_HTTP_REDIRECT), - (f"{SITE_URL}/saml2/ls/post", - saml2.BINDING_HTTP_POST), + (f"{SITE_URL}/saml2/ls/", saml2.BINDING_HTTP_REDIRECT), + (f"{SITE_URL}/saml2/ls/post", saml2.BINDING_HTTP_POST), ], }, - # attributes that this project need to identify a user "required_attributes": ["Email", "UserName"], - # attributes that may be useful to have but not required "optional_attributes": ["Firstname", "Lastname"], - # in this section the list of IdPs we talk to are defined # This is not mandatory! All the IdP available in the metadata will be considered. # 'idp': { # # we do not need a WAYF service since there is # # only an IdP defined here. This IdP should be # # present in our metadata - # # the keys of this dictionary are entity ids # 'https://localhost/simplesaml/saml2/idp/metadata.php': { # 'single_sign_on_service': { @@ -1015,36 +1042,35 @@ def saml2_attrib_map_format(dict): # }, }, }, - # where the remote metadata is stored, local, remote or mdq server. # One metadatastore or many ... "metadata": SAML_METADATA, - # set to 1 to output debugging information "debug": 0, - # Signing # 'key_file': path.join(BASEDIR, 'private.key'), # private part # 'cert_file': path.join(BASEDIR, 'public.pem'), # public part - # Encryption # 'encryption_keypairs': [{ # 'key_file': path.join(BASEDIR, 'private.key'), # private part # 'cert_file': path.join(BASEDIR, 'public.pem'), # public part # }], - # own metadata settings "contact_person": [ - {"given_name": "Lorenzo", - "sur_name": "Gil", - "company": "Yaco Sistemas", - "email_address": "lgs@yaco.es", - "contact_type": "technical"}, - {"given_name": "Angel", - "sur_name": "Fernandez", - "company": "Yaco Sistemas", - "email_address": "angel@yaco.es", - "contact_type": "administrative"}, + { + "given_name": "Lorenzo", + "sur_name": "Gil", + "company": "Yaco Sistemas", + "email_address": "lgs@yaco.es", + "contact_type": "technical", + }, + { + "given_name": "Angel", + "sur_name": "Fernandez", + "company": "Yaco Sistemas", + "email_address": "angel@yaco.es", + "contact_type": "administrative", + }, ], # you can set multilanguage information here "organization": { @@ -1083,9 +1109,9 @@ def saml2_attrib_map_format(dict): break if AUTH_REMOTEUSER_ENABLED: - REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = \ - ("dojo.remote_user.RemoteUserAuthentication",) + \ - REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] + REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ("dojo.remote_user.RemoteUserAuthentication",) + REST_FRAMEWORK[ + "DEFAULT_AUTHENTICATION_CLASSES" + ] # ------------------------------------------------------------------------------ # CELERY @@ -1115,7 +1141,7 @@ def saml2_attrib_map_format(dict): if len(env("DD_CELERY_BROKER_TRANSPORT_OPTIONS")) > 0: CELERY_BROKER_TRANSPORT_OPTIONS = json.loads(env("DD_CELERY_BROKER_TRANSPORT_OPTIONS")) -CELERY_IMPORTS = ("dojo.tools.tool_issue_updater", ) +CELERY_IMPORTS = ("dojo.tools.tool_issue_updater",) # Celery beat scheduled tasks CELERY_BEAT_SCHEDULE = { @@ -1162,7 +1188,6 @@ def saml2_attrib_map_format(dict): # 'task': 'dojo.tasks.fix_loop_duplicates_task', # 'schedule': timedelta(hours=12) # }, - } # ------------------------------------ @@ -1213,10 +1238,21 @@ def saml2_attrib_map_format(dict): "Coverity Scan JSON Report": ["title", "cwe", "line", "file_path", "description"], "SonarQube Scan": ["cwe", "severity", "file_path"], "SonarQube API Import": ["title", "file_path", "line"], - "Sonatype Application Scan": ["title", "cwe", "file_path", "component_name", "component_version", "vulnerability_ids"], + "Sonatype Application Scan": [ + "title", + "cwe", + "file_path", + "component_name", + "component_version", + "vulnerability_ids", + ], "Dependency Check Scan": ["title", "cwe", "file_path"], "Dockle Scan": ["title", "description", "vuln_id_from_tool"], - "Dependency Track Finding Packaging Format (FPF) Export": ["component_name", "component_version", "vulnerability_ids"], + "Dependency Track Finding Packaging Format (FPF) Export": [ + "component_name", + "component_version", + "vulnerability_ids", + ], "Mobsfscan Scan": ["title", "severity", "cwe"], "Tenable Scan": ["title", "severity", "vulnerability_ids", "cwe", "description"], "Nexpose Scan": ["title", "severity", "vulnerability_ids", "cwe"], @@ -1241,11 +1277,20 @@ def saml2_attrib_map_format(dict): "Trivy Scan": ["title", "severity", "vulnerability_ids", "cwe", "description"], "TFSec Scan": ["severity", "vuln_id_from_tool", "file_path", "line"], "Snyk Scan": ["vuln_id_from_tool", "file_path", "component_name", "component_version"], - "GitLab Dependency Scanning Report": ["title", "vulnerability_ids", "file_path", "component_name", "component_version"], + "GitLab Dependency Scanning Report": [ + "title", + "vulnerability_ids", + "file_path", + "component_name", + "component_version", + ], "SpotBugs Scan": ["cwe", "severity", "file_path", "line"], "JFrog Xray Unified Scan": ["vulnerability_ids", "file_path", "component_name", "component_version"], "JFrog Xray On Demand Binary Scan": ["title", "component_name", "component_version"], - "Scout Suite Scan": ["file_path", "vuln_id_from_tool"], # for now we use file_path as there is no attribute for "service" + "Scout Suite Scan": [ + "file_path", + "vuln_id_from_tool", + ], # for now we use file_path as there is no attribute for "service" "Meterian Scan": ["cwe", "component_name", "component_version", "description", "severity"], "Github Vulnerability Scan": ["title", "severity", "component_name", "vulnerability_ids", "file_path"], "Solar Appscreener Scan": ["title", "file_path", "line", "severity"], @@ -1302,7 +1347,9 @@ def saml2_attrib_map_format(dict): env_hashcode_fields_per_scanner = json.loads(env("DD_HASHCODE_FIELDS_PER_SCANNER")) for key, value in env_hashcode_fields_per_scanner.items(): if key in HASHCODE_FIELDS_PER_SCANNER: - logger.info(f"Replacing {key} with value {value} (previously set to {HASHCODE_FIELDS_PER_SCANNER[key]}) from env var DD_HASHCODE_FIELDS_PER_SCANNER") + logger.info( + f"Replacing {key} with value {value} (previously set to {HASHCODE_FIELDS_PER_SCANNER[key]}) from env var DD_HASHCODE_FIELDS_PER_SCANNER", + ) HASHCODE_FIELDS_PER_SCANNER[key] = value if key not in HASHCODE_FIELDS_PER_SCANNER: logger.info(f"Adding {key} with value {value} from env var DD_HASHCODE_FIELDS_PER_SCANNER") @@ -1363,7 +1410,22 @@ def saml2_attrib_map_format(dict): # List of fields that are known to be usable in hash_code computation) # 'endpoints' is a pseudo field that uses the endpoints (for dynamic scanners) # 'unique_id_from_tool' is often not needed here as it can be used directly in the dedupe algorithm, but it's also possible to use it for hashing -HASHCODE_ALLOWED_FIELDS = ["title", "cwe", "vulnerability_ids", "line", "file_path", "payload", "component_name", "component_version", "description", "endpoints", "unique_id_from_tool", "severity", "vuln_id_from_tool", "mitigation"] +HASHCODE_ALLOWED_FIELDS = [ + "title", + "cwe", + "vulnerability_ids", + "line", + "file_path", + "payload", + "component_name", + "component_version", + "description", + "endpoints", + "unique_id_from_tool", + "severity", + "vuln_id_from_tool", + "mitigation", +] # Adding fields to the hash_code calculation regardless of the previous settings HASH_CODE_FIELDS_ALWAYS = ["service"] @@ -1533,7 +1595,9 @@ def saml2_attrib_map_format(dict): env_dedup_algorithm_per_parser = json.loads(env("DD_DEDUPLICATION_ALGORITHM_PER_PARSER")) for key, value in env_dedup_algorithm_per_parser.items(): if key in DEDUPLICATION_ALGORITHM_PER_PARSER: - logger.info(f"Replacing {key} with value {value} (previously set to {DEDUPLICATION_ALGORITHM_PER_PARSER[key]}) from env var DD_DEDUPLICATION_ALGORITHM_PER_PARSER") + logger.info( + f"Replacing {key} with value {value} (previously set to {DEDUPLICATION_ALGORITHM_PER_PARSER[key]}) from env var DD_DEDUPLICATION_ALGORITHM_PER_PARSER", + ) DEDUPLICATION_ALGORITHM_PER_PARSER[key] = value if key not in DEDUPLICATION_ALGORITHM_PER_PARSER: logger.info(f"Adding {key} with value {value} from env var DD_DEDUPLICATION_ALGORITHM_PER_PARSER") @@ -1701,7 +1765,10 @@ def saml2_attrib_map_format(dict): ) # using 'element' for width should take width from css defined in template, but it doesn't. So set to 70% here. -TAGULOUS_AUTOCOMPLETE_SETTINGS = {"placeholder": "Enter some tags (comma separated, use enter to select / create a new tag)", "width": "70%"} +TAGULOUS_AUTOCOMPLETE_SETTINGS = { + "placeholder": "Enter some tags (comma separated, use enter to select / create a new tag)", + "width": "70%", +} EDITABLE_MITIGATED_DATA = env("DD_EDITABLE_MITIGATED_DATA") diff --git a/dojo/templates/dojo/login.html b/dojo/templates/dojo/login.html index fe155f64211..61f49190b83 100644 --- a/dojo/templates/dojo/login.html +++ b/dojo/templates/dojo/login.html @@ -45,6 +45,14 @@

{% trans "Login" %}

{% endif %}
+ {% if OIDC_ENABLED is True %} + + {% endif %} + {% if GOOGLE_ENABLED is True %}
{% trans "Login with Google" %} diff --git a/dojo/user/views.py b/dojo/user/views.py index f43b6b7b600..475679055ba 100644 --- a/dojo/user/views.py +++ b/dojo/user/views.py @@ -126,6 +126,7 @@ def login_view(request): settings.AUTH0_OAUTH2_ENABLED, settings.KEYCLOAK_OAUTH2_ENABLED, settings.GITHUB_ENTERPRISE_OAUTH2_ENABLED, + settings.OIDC_AUTH_ENABLED, settings.SAML2_ENABLED, ]) == 1 and "force_login_form" not in request.GET: if settings.GOOGLE_OAUTH_ENABLED: @@ -140,6 +141,8 @@ def login_view(request): social_auth = "keycloak" elif settings.AUTH0_OAUTH2_ENABLED: social_auth = "auth0" + elif settings.OIDC_AUTH_ENABLED: + social_auth = "oidc" elif settings.GITHUB_ENTERPRISE_OAUTH2_ENABLED: social_auth = "github-enterprise" else: