Compare commits

...

15 Commits

Author SHA1 Message Date
Gabriel Dutra
253468d050 Incorporate SAML type with Enabled setting 2020-10-14 15:36:43 -03:00
Christopher Grant
6e78bddf4c reconcile upstream master and christophergrant/master fork 2020-09-29 15:24:39 -07:00
Christopher Grant
6339665a34 move mustache template for inline saml metadata to the global level 2020-09-29 09:58:26 -07:00
Christopher Grant
420f444614 use mustache_render instead of string formatting. put all static logic into static branch 2020-09-28 12:51:35 -07:00
Christopher Grant
e1a5418364 add logging for entityid for validation 2020-09-22 10:43:35 -07:00
Christopher Grant
f694f97165 pysaml2's entityid should point to the sp, not the idp 2020-09-21 17:24:46 -07:00
Christopher Grant
219d914a24 format SAML Javascript, revert want_signed_response to pre-PR value 2020-09-14 16:27:53 -07:00
Christopher Grant
7553690922 Use utility to find xmlsec binary for encryption, formatting saml_auth module 2020-09-14 16:24:12 -07:00
Christopher Grant
340980e153 remove print debug statement 2020-09-12 01:47:42 -07:00
Christopher Grant
15208cce66 add ability to encrypt/decrypt SAML assertions with pem and crt files. Upgraded to pysaml2 6.1.0 to mitigate signature mismatch during decryption 2020-09-12 01:46:45 -07:00
Christopher Grant
9556560cb4 Fixed config of backend static/dynamic to match UI 2020-09-11 14:11:47 -07:00
Christopher Grant
0ad99bd714 add switch for static and dynamic SAML configurations 2020-09-11 00:00:38 -07:00
Christopher Grant
8a2f90ffe8 Merge pull request #1 from christophergrant/saml_with_cert
Add changes to use inline metadata.
2020-09-10 10:26:23 -07:00
Chad Chen
b1040a5f9a Add changes to use inline metadata. 2020-09-09 16:33:35 -07:00
Christopher Grant
6ac181666b Change front-end and data model for SAML2 auth - static configuration 2020-09-08 16:55:34 -07:00
5 changed files with 114 additions and 35 deletions

View File

@@ -1,45 +1,80 @@
import React from "react"; import React from "react";
import Form from "antd/lib/form"; import Form from "antd/lib/form";
import Checkbox from "antd/lib/checkbox";
import Input from "antd/lib/input"; import Input from "antd/lib/input";
import Radio from "antd/lib/radio";
import DynamicComponent from "@/components/DynamicComponent"; import DynamicComponent from "@/components/DynamicComponent";
import { SettingsEditorPropTypes, SettingsEditorDefaultProps } from "../prop-types"; import { SettingsEditorPropTypes, SettingsEditorDefaultProps } from "../prop-types";
export default function SAMLSettings(props) { export default function SAMLSettings(props) {
const { values, onChange } = props; const { values, onChange } = props;
const onChangeEnabledStatus = e => {
const updates = { auth_saml_enabled: !!e.target.value };
if (e.target.value) {
updates.auth_saml_type = e.target.value;
}
onChange(updates);
};
return ( return (
<DynamicComponent name="OrganizationSettings.SAMLSettings" {...props}> <DynamicComponent name="OrganizationSettings.SAMLSettings" {...props}>
<h4>SAML</h4> <h4>SAML</h4>
<Form.Item label="SAML Enabled"> <Form.Item label="SAML Enabled">
<Checkbox <Radio.Group
name="auth_saml_enabled" onChange={onChangeEnabledStatus}
checked={values.auth_saml_enabled} value={values.auth_saml_enabled && (values.auth_saml_type || "static")}>
onChange={e => onChange({ auth_saml_enabled: e.target.checked })}> <Radio value={false}>Disabled</Radio>
SAML Enabled <Radio value={"static"}>Enabled (Static)</Radio>
</Checkbox> <Radio value={"dynamic"}>Enabled (Dynamic)</Radio>
</Radio.Group>
</Form.Item> </Form.Item>
{values.auth_saml_enabled && ( {values.auth_saml_enabled && (
<div> <>
<Form.Item label="SAML Metadata URL"> {values.auth_saml_type === "static" && (
<Input <>
value={values.auth_saml_metadata_url} <Form.Item label="SAML Single Sign-on URL">
onChange={e => onChange({ auth_saml_metadata_url: e.target.value })} <Input
/> value={values.auth_saml_sso_url}
</Form.Item> onChange={e => onChange({ auth_saml_sso_url: e.target.value })}
<Form.Item label="SAML Entity ID"> />
<Input </Form.Item>
value={values.auth_saml_entity_id} <Form.Item label="SAML Entity ID">
onChange={e => onChange({ auth_saml_entity_id: e.target.value })} <Input
/> value={values.auth_saml_entity_id}
</Form.Item> onChange={e => onChange({ auth_saml_entity_id: e.target.value })}
<Form.Item label="SAML NameID Format"> />
<Input </Form.Item>
value={values.auth_saml_nameid_format} <Form.Item label="SAML x509 cert">
onChange={e => onChange({ auth_saml_nameid_format: e.target.value })} <Input
/> value={values.auth_saml_x509_cert}
</Form.Item> onChange={e => onChange({ auth_saml_x509_cert: e.target.value })}
</div> />
</Form.Item>
</>
)}
{values.auth_saml_type === "dynamic" && (
<>
<Form.Item label="SAML Metadata URL">
<Input
value={values.auth_saml_metadata_url}
onChange={e => onChange({ auth_saml_metadata_url: e.target.value })}
/>
</Form.Item>
<Form.Item label="SAML Entity ID">
<Input
value={values.auth_saml_entity_id}
onChange={e => onChange({ auth_saml_entity_id: e.target.value })}
/>
</Form.Item>
<Form.Item label="SAML NameID Format">
<Input
value={values.auth_saml_nameid_format}
onChange={e => onChange({ auth_saml_nameid_format: e.target.value })}
/>
</Form.Item>
</>
)}
</>
)} )}
</DynamicComponent> </DynamicComponent>
); );

View File

@@ -4,13 +4,17 @@ from redash import settings
from redash.authentication import create_and_login_user, logout_and_redirect_to_index from redash.authentication import create_and_login_user, logout_and_redirect_to_index
from redash.authentication.org_resolving import current_org from redash.authentication.org_resolving import current_org
from redash.handlers.base import org_scoped_rule from redash.handlers.base import org_scoped_rule
from redash.utils import mustache_render
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT, entity from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT, entity
from saml2.client import Saml2Client from saml2.client import Saml2Client
from saml2.config import Config as Saml2Config from saml2.config import Config as Saml2Config
from saml2.saml import NAMEID_FORMAT_TRANSIENT from saml2.saml import NAMEID_FORMAT_TRANSIENT
from saml2.sigver import get_xmlsec_binary
logger = logging.getLogger("saml_auth") logger = logging.getLogger("saml_auth")
blueprint = Blueprint("saml_auth", __name__) blueprint = Blueprint("saml_auth", __name__)
inline_metadata_template = """<?xml version="1.0" encoding="UTF-8"?><md:EntityDescriptor entityID="{{entity_id}}" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"><md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:X509Data><ds:X509Certificate>{{x509_cert}}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="{{sso_url}}"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="{{sso_url}}"/></md:IDPSSODescriptor></md:EntityDescriptor>"""
def get_saml_client(org): def get_saml_client(org):
@@ -19,12 +23,20 @@ def get_saml_client(org):
The configuration is a hash for use by saml2.config.Config The configuration is a hash for use by saml2.config.Config
""" """
metadata_url = org.get_setting("auth_saml_metadata_url")
saml_type = org.get_setting("auth_saml_type")
entity_id = org.get_setting("auth_saml_entity_id") entity_id = org.get_setting("auth_saml_entity_id")
sso_url = org.get_setting("auth_saml_sso_url")
x509_cert = org.get_setting("auth_saml_x509_cert")
metadata_url = org.get_setting("auth_saml_metadata_url")
if settings.SAML_SCHEME_OVERRIDE: if settings.SAML_SCHEME_OVERRIDE:
acs_url = url_for("saml_auth.idp_initiated", org_slug=org.slug, _external=True, acs_url = url_for(
_scheme=settings.SAML_SCHEME_OVERRIDE) "saml_auth.idp_initiated",
org_slug=org.slug,
_external=True,
_scheme=settings.SAML_SCHEME_OVERRIDE,
)
else: else:
acs_url = url_for("saml_auth.idp_initiated", org_slug=org.slug, _external=True) acs_url = url_for("saml_auth.idp_initiated", org_slug=org.slug, _external=True)
@@ -51,8 +63,30 @@ def get_saml_client(org):
}, },
} }
if entity_id is not None and entity_id != "": if settings.SAML_ENCRYPTION_ENABLED:
saml_settings["entityid"] = entity_id encryption_dict = {
"xmlsec_binary": get_xmlsec_binary(),
"encryption_keypairs": [
{
"key_file": settings.SAML_ENCRYPTION_PEM_PATH,
"cert_file": settings.SAML_ENCRYPTION_CERT_PATH,
}
],
}
saml_settings.update(encryption_dict)
if saml_type is not None and saml_type == "static":
metadata_inline = mustache_render(
inline_metadata_template,
entity_id=entity_id,
x509_cert=x509_cert,
sso_url=sso_url,
)
saml_settings["metadata"] = {"inline": [metadata_inline]}
if acs_url is not None and acs_url != "":
saml_settings["entityid"] = acs_url
sp_config = Saml2Config() sp_config = Saml2Config()
sp_config.load(saml_settings) sp_config.load(saml_settings)

View File

@@ -167,6 +167,10 @@ GOOGLE_OAUTH_ENABLED = bool(GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET)
# This setting will force the URL scheme. # This setting will force the URL scheme.
SAML_SCHEME_OVERRIDE = os.environ.get("REDASH_SAML_SCHEME_OVERRIDE", "") SAML_SCHEME_OVERRIDE = os.environ.get("REDASH_SAML_SCHEME_OVERRIDE", "")
SAML_ENCRYPTION_PEM_PATH = os.environ.get("REDASH_SAML_ENCRYPTION_PEM_PATH", "")
SAML_ENCRYPTION_CERT_PATH = os.environ.get("REDASH_SAML_ENCRYPTION_CERT_PATH", "")
SAML_ENCRYPTION_ENABLED = SAML_ENCRYPTION_PEM_PATH != "" and SAML_ENCRYPTION_CERT_PATH != ""
# Enables the use of an externally-provided and trusted remote user via an HTTP # Enables the use of an externally-provided and trusted remote user via an HTTP
# header. The "user" must be an email address. # header. The "user" must be an email address.
# #

View File

@@ -14,10 +14,13 @@ PASSWORD_LOGIN_ENABLED = parse_boolean(
os.environ.get("REDASH_PASSWORD_LOGIN_ENABLED", "true") os.environ.get("REDASH_PASSWORD_LOGIN_ENABLED", "true")
) )
SAML_LOGIN_TYPE = os.environ.get("REDASH_SAML_AUTH_TYPE", "")
SAML_METADATA_URL = os.environ.get("REDASH_SAML_METADATA_URL", "") SAML_METADATA_URL = os.environ.get("REDASH_SAML_METADATA_URL", "")
SAML_ENTITY_ID = os.environ.get("REDASH_SAML_ENTITY_ID", "") SAML_ENTITY_ID = os.environ.get("REDASH_SAML_ENTITY_ID", "")
SAML_NAMEID_FORMAT = os.environ.get("REDASH_SAML_NAMEID_FORMAT", "") SAML_NAMEID_FORMAT = os.environ.get("REDASH_SAML_NAMEID_FORMAT", "")
SAML_LOGIN_ENABLED = SAML_METADATA_URL != "" SAML_SSO_URL = os.environ.get("REDASH_SAML_SSO_URL", "")
SAML_X509_CERT = os.environ.get("REDASH_SAML_X509_CERT", "")
SAML_LOGIN_ENABLED = SAML_SSO_URL != "" and SAML_METADATA_URL != ""
DATE_FORMAT = os.environ.get("REDASH_DATE_FORMAT", "DD/MM/YY") DATE_FORMAT = os.environ.get("REDASH_DATE_FORMAT", "DD/MM/YY")
TIME_FORMAT = os.environ.get("REDASH_TIME_FORMAT", "HH:mm") TIME_FORMAT = os.environ.get("REDASH_TIME_FORMAT", "HH:mm")
@@ -52,9 +55,12 @@ settings = {
"beacon_consent": None, "beacon_consent": None,
"auth_password_login_enabled": PASSWORD_LOGIN_ENABLED, "auth_password_login_enabled": PASSWORD_LOGIN_ENABLED,
"auth_saml_enabled": SAML_LOGIN_ENABLED, "auth_saml_enabled": SAML_LOGIN_ENABLED,
"auth_saml_type": SAML_LOGIN_TYPE,
"auth_saml_entity_id": SAML_ENTITY_ID, "auth_saml_entity_id": SAML_ENTITY_ID,
"auth_saml_metadata_url": SAML_METADATA_URL, "auth_saml_metadata_url": SAML_METADATA_URL,
"auth_saml_nameid_format": SAML_NAMEID_FORMAT, "auth_saml_nameid_format": SAML_NAMEID_FORMAT,
"auth_saml_sso_url": SAML_SSO_URL,
"auth_saml_x509_cert": SAML_X509_CERT,
"date_format": DATE_FORMAT, "date_format": DATE_FORMAT,
"time_format": TIME_FORMAT, "time_format": TIME_FORMAT,
"integer_format": INTEGER_FORMAT, "integer_format": INTEGER_FORMAT,

View File

@@ -39,7 +39,7 @@ rq==1.1.0
rq-scheduler==0.9.1 rq-scheduler==0.9.1
jsonschema==3.1.1 jsonschema==3.1.1
RestrictedPython==5.0 RestrictedPython==5.0
pysaml2==4.8.0 pysaml2==6.1.0
pycrypto==2.6.1 pycrypto==2.6.1
funcy==1.13 funcy==1.13
sentry-sdk>=0.14.3,<0.15.0 sentry-sdk>=0.14.3,<0.15.0