SAML2 (#83)
* feat(api): remove stage name from email subject * change(api): refactored code & SAML2 SSO SLO SLS * change(api): SAML2 extracted & custom configuration * change(api): SAML2 migrate user after signup * feat(api): return project_key with session's details * change(api): SAML2 * feat(db): tenants & users table changes for SAML2
This commit is contained in:
parent
376b110cdd
commit
9a5fc4bac7
35 changed files with 533 additions and 70 deletions
|
|
@ -10,7 +10,7 @@ from chalicelib.utils.helper import environ
|
|||
|
||||
|
||||
def __get_subject(subject):
|
||||
return subject if helper.is_production() else f"{helper.get_stage_name()}: {subject}"
|
||||
return subject
|
||||
|
||||
|
||||
def __get_html_from_file(source, formatting_variables):
|
||||
|
|
|
|||
|
|
@ -55,7 +55,13 @@
|
|||
"S3_HOST": "",
|
||||
"S3_KEY": "",
|
||||
"S3_SECRET": "",
|
||||
"version_number": "1.0.0"
|
||||
"version_number": "1.0.0",
|
||||
"LICENSE_KEY": "",
|
||||
"SAML2_MD_URL": "",
|
||||
"idp_entityId": "",
|
||||
"idp_sso_url": "",
|
||||
"idp_x509cert": "",
|
||||
"idp_sls_url": ""
|
||||
},
|
||||
"lambda_timeout": 150,
|
||||
"lambda_memory_size": 400,
|
||||
|
|
|
|||
58
ee/api/.gitignore
vendored
58
ee/api/.gitignore
vendored
|
|
@ -175,4 +175,60 @@ SUBNETS.json
|
|||
chalicelib/.config
|
||||
chalicelib/saas
|
||||
README/*
|
||||
Pipfile
|
||||
Pipfile
|
||||
|
||||
/chalicelib/core/alerts.py
|
||||
/chalicelib/core/announcements.py
|
||||
/chalicelib/blueprints/bp_app_api.py
|
||||
/chalicelib/blueprints/bp_core.py
|
||||
/chalicelib/blueprints/bp_core_crons.py
|
||||
/chalicelib/core/collaboration_slack.py
|
||||
/chalicelib/core/errors_favorite_viewed.py
|
||||
/chalicelib/core/events.py
|
||||
/chalicelib/core/events_ios.py
|
||||
/chalicelib/core/integration_base.py
|
||||
/chalicelib/core/integration_base_issue.py
|
||||
/chalicelib/core/integration_github.py
|
||||
/chalicelib/core/integration_github_issue.py
|
||||
/chalicelib/core/integration_jira_cloud.py
|
||||
/chalicelib/core/integration_jira_cloud_issue.py
|
||||
/chalicelib/core/integrations_manager.py
|
||||
/chalicelib/core/issues.py
|
||||
/chalicelib/core/jobs.py
|
||||
/chalicelib/core/log_tool_bugsnag.py
|
||||
/chalicelib/core/log_tool_cloudwatch.py
|
||||
/chalicelib/core/log_tool_datadog.py
|
||||
/chalicelib/core/log_tool_elasticsearch.py
|
||||
/chalicelib/core/log_tool_newrelic.py
|
||||
/chalicelib/core/log_tool_rollbar.py
|
||||
/chalicelib/core/log_tool_sentry.py
|
||||
/chalicelib/core/log_tool_stackdriver.py
|
||||
/chalicelib/core/log_tool_sumologic.py
|
||||
/chalicelib/core/sessions_assignments.py
|
||||
/chalicelib/core/sessions_favorite_viewed.py
|
||||
/chalicelib/core/sessions_metas.py
|
||||
/chalicelib/core/sessions_mobs.py
|
||||
/chalicelib/core/significance.py
|
||||
/chalicelib/core/slack.py
|
||||
/chalicelib/core/socket_ios.py
|
||||
/chalicelib/core/sourcemaps.py
|
||||
/chalicelib/core/sourcemaps_parser.py
|
||||
/chalicelib/core/weekly_report.py
|
||||
/chalicelib/saml
|
||||
/chalicelib/utils/html/
|
||||
/chalicelib/utils/__init__.py
|
||||
/chalicelib/utils/args_transformer.py
|
||||
/chalicelib/utils/captcha.py
|
||||
/chalicelib/utils/dev.py
|
||||
/chalicelib/utils/email_handler.py
|
||||
/chalicelib/utils/email_helper.py
|
||||
/chalicelib/utils/event_filter_definition.py
|
||||
/chalicelib/utils/github_client_v3.py
|
||||
/chalicelib/utils/helper.py
|
||||
/chalicelib/utils/jira_client.py
|
||||
/chalicelib/utils/metrics_helper.py
|
||||
/chalicelib/utils/pg_client.py
|
||||
/chalicelib/utils/s3.py
|
||||
/chalicelib/utils/smtp.py
|
||||
/chalicelib/utils/strings.py
|
||||
/chalicelib/utils/TimeUTC.py
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from chalicelib.utils import helper
|
|||
from chalicelib.utils import pg_client
|
||||
from chalicelib.utils.helper import environ
|
||||
|
||||
from chalicelib.blueprints import bp_ee, bp_ee_crons
|
||||
from chalicelib.blueprints import bp_ee, bp_ee_crons, bp_saml
|
||||
|
||||
app = Chalice(app_name='parrot')
|
||||
app.debug = not helper.is_production() or helper.is_local()
|
||||
|
|
@ -59,17 +59,11 @@ _overrides.chalice_app(app)
|
|||
|
||||
@app.middleware('http')
|
||||
def or_middleware(event, get_response):
|
||||
from chalicelib.ee import unlock
|
||||
from chalicelib.core import unlock
|
||||
if not unlock.is_valid():
|
||||
return Response(body={"errors": ["expired license"]}, status_code=403)
|
||||
if "{projectid}" in event.path.lower():
|
||||
from chalicelib.ee import projects
|
||||
print("==================================")
|
||||
print(event.context["authorizer"].get("authorizer_identity"))
|
||||
print(event.uri_params["projectId"])
|
||||
print(projects.get_internal_project_id(event.uri_params["projectId"]))
|
||||
print(event.context["authorizer"]["tenantId"])
|
||||
print("==================================")
|
||||
from chalicelib.core import projects
|
||||
if event.context["authorizer"].get("authorizer_identity") == "api_key" \
|
||||
and not projects.is_authorized(
|
||||
project_id=projects.get_internal_project_id(event.uri_params["projectId"]),
|
||||
|
|
@ -126,3 +120,4 @@ app.register_blueprint(bp_dashboard.app)
|
|||
# Enterprise
|
||||
app.register_blueprint(bp_ee.app)
|
||||
app.register_blueprint(bp_ee_crons.app)
|
||||
app.register_blueprint(bp_saml.app)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from chalice import Blueprint, AuthResponse
|
|||
from chalicelib.utils import helper
|
||||
from chalicelib.core import authorizers
|
||||
|
||||
from chalicelib.ee import users
|
||||
from chalicelib.core import users
|
||||
|
||||
app = Blueprint(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,19 +3,19 @@ from chalice import Blueprint, Response
|
|||
from chalicelib import _overrides
|
||||
from chalicelib.core import metadata, errors_favorite_viewed, slack, alerts, sessions, integration_github, \
|
||||
integrations_manager
|
||||
from chalicelib.utils import captcha
|
||||
from chalicelib.utils import captcha, SAML2_helper
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib.utils.helper import environ
|
||||
|
||||
from chalicelib.ee import tenants
|
||||
from chalicelib.ee import signup
|
||||
from chalicelib.ee import users
|
||||
from chalicelib.ee import projects
|
||||
from chalicelib.ee import errors
|
||||
from chalicelib.ee import notifications
|
||||
from chalicelib.ee import boarding
|
||||
from chalicelib.ee import webhook
|
||||
from chalicelib.ee import license
|
||||
from chalicelib.core import tenants
|
||||
from chalicelib.core import signup
|
||||
from chalicelib.core import users
|
||||
from chalicelib.core import projects
|
||||
from chalicelib.core import errors
|
||||
from chalicelib.core import notifications
|
||||
from chalicelib.core import boarding
|
||||
from chalicelib.core import webhook
|
||||
from chalicelib.core import license
|
||||
from chalicelib.core.collaboration_slack import Slack
|
||||
|
||||
app = Blueprint(__name__)
|
||||
|
|
@ -41,6 +41,8 @@ def login():
|
|||
return {
|
||||
'errors': ['You’ve entered invalid Email or Password.']
|
||||
}
|
||||
elif "errors" in r:
|
||||
return r
|
||||
|
||||
tenant_id = r.pop("tenantId")
|
||||
# change this in open-source
|
||||
|
|
@ -74,7 +76,8 @@ def get_account(context):
|
|||
"metadata": metadata.get_remaining_metadata_with_count(context['tenantId'])
|
||||
},
|
||||
**license.get_status(context["tenantId"]),
|
||||
"smtp": environ["EMAIL_HOST"] is not None and len(environ["EMAIL_HOST"]) > 0
|
||||
"smtp": environ["EMAIL_HOST"] is not None and len(environ["EMAIL_HOST"]) > 0,
|
||||
"saml2": SAML2_helper.is_saml2_available()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -350,6 +353,8 @@ def get_members(context):
|
|||
|
||||
@app.route('/client/members', methods=['PUT', 'POST'])
|
||||
def add_member(context):
|
||||
if SAML2_helper.is_saml2_available():
|
||||
return {"errors": ["please use your SSO server to add teammates"]}
|
||||
data = app.current_request.json_body
|
||||
return users.create_member(tenant_id=context['tenantId'], user_id=context['userId'], data=data)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ from chalicelib.utils import helper
|
|||
|
||||
app = Blueprint(__name__)
|
||||
_overrides.chalice_app(app)
|
||||
from chalicelib.ee import telemetry
|
||||
from chalicelib.ee import unlock
|
||||
from chalicelib.core import telemetry
|
||||
from chalicelib.core import unlock
|
||||
|
||||
|
||||
# Run every day.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from chalice import Blueprint
|
||||
|
||||
from chalicelib import _overrides
|
||||
from chalicelib.ee import unlock
|
||||
from chalicelib.core import unlock
|
||||
|
||||
app = Blueprint(__name__)
|
||||
_overrides.chalice_app(app)
|
||||
|
|
|
|||
188
ee/api/chalicelib/blueprints/bp_saml.py
Normal file
188
ee/api/chalicelib/blueprints/bp_saml.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
from chalice import Blueprint
|
||||
|
||||
from chalicelib import _overrides
|
||||
from chalicelib.utils.SAML2_helper import prepare_request, init_saml_auth
|
||||
|
||||
app = Blueprint(__name__)
|
||||
_overrides.chalice_app(app)
|
||||
|
||||
from chalicelib.utils.helper import environ
|
||||
|
||||
from onelogin.saml2.auth import OneLogin_Saml2_Logout_Request
|
||||
from onelogin.saml2.utils import OneLogin_Saml2_Utils
|
||||
|
||||
from chalice import Response
|
||||
from chalicelib.core import users, tenants
|
||||
|
||||
|
||||
@app.route("/saml2", methods=['GET'], authorizer=None)
|
||||
def start_sso():
|
||||
app.current_request.path = ''
|
||||
req = prepare_request(request=app.current_request)
|
||||
auth = init_saml_auth(req)
|
||||
sso_built_url = auth.login()
|
||||
return Response(
|
||||
# status_code=301,
|
||||
status_code=307,
|
||||
body='',
|
||||
headers={'Location': sso_built_url, 'Content-Type': 'text/plain'})
|
||||
|
||||
|
||||
@app.route('/saml2/acs', methods=['POST'], content_types=['application/x-www-form-urlencoded'], authorizer=None)
|
||||
def process_sso_assertion():
|
||||
req = prepare_request(request=app.current_request)
|
||||
session = req["cookie"]["session"]
|
||||
request = req['request']
|
||||
auth = init_saml_auth(req)
|
||||
|
||||
request_id = None
|
||||
if 'AuthNRequestID' in session:
|
||||
request_id = session['AuthNRequestID']
|
||||
|
||||
auth.process_response(request_id=request_id)
|
||||
errors = auth.get_errors()
|
||||
user_data = {}
|
||||
if len(errors) == 0:
|
||||
if 'AuthNRequestID' in session:
|
||||
del session['AuthNRequestID']
|
||||
user_data = auth.get_attributes()
|
||||
# session['samlUserdata'] = user_data
|
||||
# session['samlNameId'] = auth.get_nameid()
|
||||
# session['samlNameIdFormat'] = auth.get_nameid_format()
|
||||
# session['samlNameIdNameQualifier'] = auth.get_nameid_nq()
|
||||
# session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq()
|
||||
# session['samlSessionIndex'] = auth.get_session_index()
|
||||
# session['samlSessionExpiration'] = auth.get_session_expiration()
|
||||
# print('>>>>')
|
||||
# print(session)
|
||||
self_url = OneLogin_Saml2_Utils.get_self_url(req)
|
||||
if 'RelayState' in request.form and self_url != request.form['RelayState']:
|
||||
print("====>redirect")
|
||||
return Response(
|
||||
status_code=307,
|
||||
body='',
|
||||
headers={'Location': auth.redirect_to(request.form['RelayState']), 'Content-Type': 'text/plain'})
|
||||
elif auth.get_settings().is_debug_active():
|
||||
error_reason = auth.get_last_error_reason()
|
||||
return {"errors": [error_reason]}
|
||||
|
||||
email = auth.get_nameid()
|
||||
existing = users.get_by_email_only(auth.get_nameid())
|
||||
|
||||
internal_id = next(iter(user_data.get("internalId", [])), None)
|
||||
if len(existing) == 0 or existing[0].get("origin") != 'saml':
|
||||
tenant_key = user_data.get("tenantKey", [])
|
||||
if len(tenant_key) == 0:
|
||||
print("tenantKey not present in assertion")
|
||||
return Response(
|
||||
status_code=307,
|
||||
body={"errors": ["tenantKey not present in assertion"]},
|
||||
headers={'Location': auth.redirect_to(request.form['RelayState']), 'Content-Type': 'text/plain'})
|
||||
else:
|
||||
t = tenants.get_by_tenant_key(tenant_key[0])
|
||||
if t is None:
|
||||
return Response(
|
||||
status_code=307,
|
||||
body={"errors": ["Unknown tenantKey"]},
|
||||
headers={'Location': auth.redirect_to(request.form['RelayState']), 'Content-Type': 'text/plain'})
|
||||
if len(existing) == 0:
|
||||
print("== new user ==")
|
||||
users.create_sso_user(tenant_id=t['tenantId'], email=email, admin=True, origin='saml',
|
||||
name=" ".join(user_data.get("firstName", []) + user_data.get("lastName", [])),
|
||||
internal_id=internal_id)
|
||||
else:
|
||||
existing = existing[0]
|
||||
if existing.get("origin") != 'saml':
|
||||
print("== migrating user to SAML ==")
|
||||
users.update(tenant_id=t['tenantId'], user_id=existing["id"],
|
||||
changes={"origin": 'saml', "internal_id": internal_id})
|
||||
|
||||
return users.authenticate_sso(email=email, internal_id=internal_id, exp=auth.get_session_expiration())
|
||||
|
||||
|
||||
@app.route('/saml2/slo', methods=['GET'])
|
||||
def process_slo_request(context):
|
||||
req = prepare_request(request=app.current_request)
|
||||
session = req["cookie"]["session"]
|
||||
request = req['request']
|
||||
auth = init_saml_auth(req)
|
||||
|
||||
name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None
|
||||
if 'samlNameId' in session:
|
||||
name_id = session['samlNameId']
|
||||
if 'samlSessionIndex' in session:
|
||||
session_index = session['samlSessionIndex']
|
||||
if 'samlNameIdFormat' in session:
|
||||
name_id_format = session['samlNameIdFormat']
|
||||
if 'samlNameIdNameQualifier' in session:
|
||||
name_id_nq = session['samlNameIdNameQualifier']
|
||||
if 'samlNameIdSPNameQualifier' in session:
|
||||
name_id_spnq = session['samlNameIdSPNameQualifier']
|
||||
users.change_jwt_iat(context["userId"])
|
||||
return Response(
|
||||
status_code=307,
|
||||
body='',
|
||||
headers={'Location': auth.logout(name_id=name_id, session_index=session_index, nq=name_id_nq,
|
||||
name_id_format=name_id_format,
|
||||
spnq=name_id_spnq), 'Content-Type': 'text/plain'})
|
||||
|
||||
|
||||
@app.route('/saml2/sls', methods=['GET'], authorizer=None)
|
||||
def process_sls_assertion():
|
||||
req = prepare_request(request=app.current_request)
|
||||
session = req["cookie"]["session"]
|
||||
request = req['request']
|
||||
auth = init_saml_auth(req)
|
||||
request_id = None
|
||||
if 'LogoutRequestID' in session:
|
||||
request_id = session['LogoutRequestID']
|
||||
|
||||
def dscb():
|
||||
session.clear()
|
||||
|
||||
url = auth.process_slo(request_id=request_id, delete_session_cb=dscb)
|
||||
|
||||
errors = auth.get_errors()
|
||||
if len(errors) == 0:
|
||||
if 'SAMLRequest' in req['get_data']:
|
||||
logout_request = OneLogin_Saml2_Logout_Request(auth.get_settings(), req['get_data']['SAMLRequest'])
|
||||
user_email = logout_request.get_nameid(auth.get_last_request_xml())
|
||||
to_logout = users.get_by_email_only(user_email)
|
||||
|
||||
if len(to_logout) > 0:
|
||||
to_logout = to_logout[0]['id']
|
||||
users.change_jwt_iat(to_logout)
|
||||
else:
|
||||
print("Unknown user SLS-Request By IdP")
|
||||
else:
|
||||
print("Preprocessed SLS-Request by SP")
|
||||
|
||||
if url is not None:
|
||||
return Response(
|
||||
status_code=307,
|
||||
body='',
|
||||
headers={'Location': url, 'Content-Type': 'text/plain'})
|
||||
|
||||
return Response(
|
||||
status_code=307,
|
||||
body='',
|
||||
headers={'Location': environ["SITE_URL"], 'Content-Type': 'text/plain'})
|
||||
|
||||
|
||||
@app.route('/saml2/metadata', methods=['GET'], authorizer=None)
|
||||
def saml2_metadata():
|
||||
req = prepare_request(request=app.current_request)
|
||||
auth = init_saml_auth(req)
|
||||
settings = auth.get_settings()
|
||||
metadata = settings.get_sp_metadata()
|
||||
errors = settings.validate_metadata(metadata)
|
||||
|
||||
if len(errors) == 0:
|
||||
return Response(
|
||||
status_code=200,
|
||||
body=metadata,
|
||||
headers={'Content-Type': 'text/xml'})
|
||||
else:
|
||||
return Response(
|
||||
status_code=500,
|
||||
body=', '.join(errors))
|
||||
|
|
@ -2,7 +2,7 @@ from chalice import Blueprint
|
|||
from chalicelib.utils import helper
|
||||
from chalicelib import _overrides
|
||||
|
||||
from chalicelib.ee import dashboard
|
||||
from chalicelib.core import dashboard
|
||||
|
||||
from chalicelib.core import metadata
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import jwt
|
|||
from chalicelib.utils import helper
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
from chalicelib.ee import tenants
|
||||
from chalicelib.ee import users
|
||||
from chalicelib.core import tenants
|
||||
from chalicelib.core import users
|
||||
|
||||
|
||||
def jwt_authorizer(token):
|
||||
|
|
@ -38,12 +38,13 @@ def jwt_context(context):
|
|||
}
|
||||
|
||||
|
||||
def generate_jwt(id, tenant_id, iat, aud):
|
||||
def generate_jwt(id, tenant_id, iat, aud, exp=None):
|
||||
token = jwt.encode(
|
||||
payload={
|
||||
"userId": id,
|
||||
"tenantId": tenant_id,
|
||||
"exp": iat // 1000 + int(environ["jwt_exp_delta_seconds"]) + TimeUTC.get_utc_offset() // 1000,
|
||||
"exp": iat // 1000 + int(environ["jwt_exp_delta_seconds"]) + TimeUTC.get_utc_offset() // 1000 \
|
||||
if exp is None else exp,
|
||||
"iss": environ["jwt_issuer"],
|
||||
"iat": iat // 1000,
|
||||
"aud": aud
|
||||
|
|
@ -58,4 +59,4 @@ def api_key_authorizer(token):
|
|||
t = tenants.get_by_api_key(token)
|
||||
if t is not None:
|
||||
t["createdAt"] = TimeUTC.datetime_to_timestamp(t["createdAt"])
|
||||
return t
|
||||
return t
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from chalicelib.utils import pg_client
|
||||
from chalicelib.core import log_tool_datadog, log_tool_stackdriver, log_tool_sentry
|
||||
|
||||
from chalicelib.ee import users
|
||||
from chalicelib.ee import projects
|
||||
from chalicelib.core import users
|
||||
from chalicelib.core import projects
|
||||
|
||||
|
||||
def get_state(tenant_id):
|
||||
|
|
@ -5,7 +5,7 @@ from chalicelib.utils import pg_client
|
|||
from chalicelib.utils import args_transformer
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
from chalicelib.ee.utils import ch_client
|
||||
from chalicelib.utils import ch_client
|
||||
from math import isnan
|
||||
from chalicelib.utils.metrics_helper import __get_step_size
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import json
|
||||
|
||||
from chalicelib.utils import pg_client, helper
|
||||
from chalicelib.ee.utils import ch_client
|
||||
from chalicelib.utils import ch_client
|
||||
from chalicelib.core import sourcemaps, sessions
|
||||
from chalicelib.ee import dashboard
|
||||
from chalicelib.core import dashboard
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
from chalicelib.utils import pg_client
|
||||
from chalicelib.ee import unlock
|
||||
from chalicelib.core import unlock
|
||||
|
||||
|
||||
def get_status(tenant_id):
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from chalicelib.utils import pg_client, helper, dev
|
||||
|
||||
from chalicelib.ee import projects
|
||||
from chalicelib.core import projects
|
||||
|
||||
import re
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
|
||||
from chalicelib.ee import users
|
||||
from chalicelib.core import users
|
||||
from chalicelib.utils import pg_client, helper
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ from chalicelib.utils import email_helper, captcha, helper
|
|||
import secrets
|
||||
from chalicelib.utils import pg_client
|
||||
|
||||
from chalicelib.ee import users
|
||||
from chalicelib.core import users
|
||||
|
||||
|
||||
def step1(data):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from chalicelib.utils import helper
|
||||
from chalicelib.ee.utils import ch_client
|
||||
from chalicelib.utils import ch_client
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
|
||||
|
|
@ -2,9 +2,9 @@ from chalicelib.utils import pg_client, helper
|
|||
from chalicelib.utils import dev
|
||||
from chalicelib.core import events, sessions_metas, socket_ios, metadata, events_ios, sessions_mobs, issues
|
||||
|
||||
from chalicelib.ee import projects, errors
|
||||
from chalicelib.core import projects, errors
|
||||
|
||||
from chalicelib.ee import resources
|
||||
from chalicelib.core import resources
|
||||
|
||||
SESSION_PROJECTION_COLS = """s.project_id,
|
||||
s.session_id::text AS session_id,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from chalicelib.utils import helper
|
||||
from chalicelib.utils import pg_client
|
||||
from chalicelib.ee import users, telemetry
|
||||
from chalicelib.core import users, telemetry
|
||||
from chalicelib.utils import captcha
|
||||
import json
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
|
@ -57,7 +57,8 @@ def create_step1(data):
|
|||
signed_ups = get_signed_ups()
|
||||
|
||||
if len(signed_ups) == 0 and data.get("tenantId") is not None \
|
||||
or len(signed_ups) > 0 and data.get("tenantId") not in [t['tenantId'] for t in signed_ups]:
|
||||
or len(signed_ups) > 0 and data.get("tenantId") is not None\
|
||||
and data.get("tenantId") not in [t['tenantId'] for t in signed_ups]:
|
||||
errors.append("Tenant does not exist")
|
||||
if len(errors) > 0:
|
||||
print("==> error")
|
||||
|
|
@ -156,4 +157,4 @@ def create_step1(data):
|
|||
"user": r,
|
||||
"client": c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -65,4 +65,4 @@ def new_client(tenant_id):
|
|||
FROM public.tenants
|
||||
WHERE tenant_id=%(tenant_id)s;""", {"tenant_id": tenant_id}))
|
||||
data = cur.fetchone()
|
||||
requests.post('https://parrot.asayer.io/os/signup', json=process_data(data, edition='ee'))
|
||||
requests.post('https://parrot.asayer.io/os/signup', json=process_data(data, edition='ee'))
|
||||
|
|
@ -1,6 +1,26 @@
|
|||
from chalicelib.utils import pg_client
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib.ee import users
|
||||
from chalicelib.core import users
|
||||
|
||||
|
||||
def get_by_tenant_key(tenant_key):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(
|
||||
cur.mogrify(
|
||||
f"""SELECT
|
||||
t.tenant_id,
|
||||
t.name,
|
||||
t.api_key,
|
||||
t.created_at,
|
||||
t.edition,
|
||||
t.version_number,
|
||||
t.opt_out
|
||||
FROM public.tenants AS t
|
||||
WHERE t.user_id = %(user_id)s AND t.deleted_at ISNULL
|
||||
LIMIT 1;""",
|
||||
{"user_id": tenant_key})
|
||||
)
|
||||
return helper.dict_to_camel_case(cur.fetchone())
|
||||
|
||||
|
||||
def get_by_tenant_id(tenant_id):
|
||||
|
|
@ -14,7 +34,8 @@ def get_by_tenant_id(tenant_id):
|
|||
t.created_at,
|
||||
t.edition,
|
||||
t.version_number,
|
||||
t.opt_out
|
||||
t.opt_out,
|
||||
t.user_id AS tenant_key
|
||||
FROM public.tenants AS t
|
||||
WHERE t.tenant_id = %(tenantId)s AND t.deleted_at ISNULL
|
||||
LIMIT 1;""",
|
||||
|
|
@ -1,15 +1,12 @@
|
|||
import json
|
||||
import time
|
||||
|
||||
from chalicelib.core import authorizers
|
||||
|
||||
from chalicelib.core import tenants
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib.utils import pg_client
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
from chalicelib.utils.helper import environ
|
||||
|
||||
from chalicelib.ee import tenants
|
||||
|
||||
|
||||
def create_new_member(tenant_id, email, password, admin, name, owner=False):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
|
|
@ -203,7 +200,8 @@ def get(user_id, tenant_id):
|
|||
(CASE WHEN role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||
(CASE WHEN role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
||||
appearance,
|
||||
api_key
|
||||
api_key,
|
||||
origin
|
||||
FROM public.users LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id
|
||||
WHERE
|
||||
users.user_id = %(userId)s
|
||||
|
|
@ -274,7 +272,8 @@ def get_by_email_only(email):
|
|||
basic_authentication.generated_password,
|
||||
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||
(CASE WHEN users.role = 'member' THEN TRUE ELSE FALSE END) AS member
|
||||
(CASE WHEN users.role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
||||
origin
|
||||
FROM public.users LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id
|
||||
WHERE
|
||||
users.email = %(email)s
|
||||
|
|
@ -363,6 +362,8 @@ def change_password(tenant_id, user_id, email, old_password, new_password):
|
|||
item = get(tenant_id=tenant_id, user_id=user_id)
|
||||
if item is None:
|
||||
return {"errors": ["access denied"]}
|
||||
if item["origin"] is not None:
|
||||
return {"errors": ["cannot change your password because you are logged-in form an SSO service"]}
|
||||
if old_password == new_password:
|
||||
return {"errors": ["old and new password are the same"]}
|
||||
auth = authenticate(email, old_password, for_change_password=True)
|
||||
|
|
@ -437,9 +438,19 @@ def auth_exists(user_id, tenant_id, jwt_iat, jwt_aud):
|
|||
)
|
||||
|
||||
|
||||
def change_jwt_iat(user_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
f"""UPDATE public.users
|
||||
SET jwt_iat = timezone('utc'::text, now())
|
||||
WHERE user_id = %(user_id)s
|
||||
RETURNING jwt_iat;""",
|
||||
{"user_id": user_id})
|
||||
cur.execute(query)
|
||||
return cur.fetchone().get("jwt_iat")
|
||||
|
||||
|
||||
def authenticate(email, password, for_change_password=False, for_plugin=False):
|
||||
if helper.TRACK_TIME:
|
||||
now = int(time.time() * 1000)
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
f"""SELECT
|
||||
|
|
@ -451,7 +462,8 @@ def authenticate(email, password, for_change_password=False, for_plugin=False):
|
|||
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||
(CASE WHEN users.role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
||||
users.appearance
|
||||
users.appearance,
|
||||
users.origin
|
||||
FROM public.users AS users INNER JOIN public.basic_authentication USING(user_id)
|
||||
WHERE users.email = %(email)s
|
||||
AND basic_authentication.password = crypt(%(password)s, basic_authentication.password)
|
||||
|
|
@ -461,13 +473,45 @@ def authenticate(email, password, for_change_password=False, for_plugin=False):
|
|||
|
||||
cur.execute(query)
|
||||
r = cur.fetchone()
|
||||
if helper.TRACK_TIME:
|
||||
now2 = int(time.time() * 1000)
|
||||
print(f"=====> authentication query&fetch in: {now2 - now} ms")
|
||||
now = now2
|
||||
if r is not None:
|
||||
if r["origin"] is not None:
|
||||
return {"errors": ["must sign-in with SSO"]}
|
||||
if for_change_password:
|
||||
return True
|
||||
r = helper.dict_to_camel_case(r, ignore_keys=["appearance"])
|
||||
jwt_iat = change_jwt_iat(r['id'])
|
||||
return {
|
||||
"jwt": authorizers.generate_jwt(r['id'], r['tenantId'],
|
||||
TimeUTC.datetime_to_timestamp(jwt_iat),
|
||||
aud=f"plugin:{helper.get_stage_name()}" if for_plugin else f"front:{helper.get_stage_name()}"),
|
||||
"email": email,
|
||||
**r
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def authenticate_sso(email, internal_id, exp=None):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
f"""SELECT
|
||||
users.user_id AS id,
|
||||
users.tenant_id,
|
||||
users.role,
|
||||
users.name,
|
||||
False AS change_password,
|
||||
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||
(CASE WHEN users.role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
||||
users.appearance,
|
||||
origin
|
||||
FROM public.users AS users
|
||||
WHERE users.email = %(email)s AND internal_id = %(internal_id)s;""",
|
||||
{"email": email, "internal_id": internal_id})
|
||||
|
||||
cur.execute(query)
|
||||
r = cur.fetchone()
|
||||
|
||||
if r is not None:
|
||||
if for_change_password:
|
||||
return True
|
||||
r = helper.dict_to_camel_case(r, ignore_keys=["appearance"])
|
||||
query = cur.mogrify(
|
||||
f"""UPDATE public.users
|
||||
|
|
@ -479,8 +523,37 @@ def authenticate(email, password, for_change_password=False, for_plugin=False):
|
|||
return {
|
||||
"jwt": authorizers.generate_jwt(r['id'], r['tenantId'],
|
||||
TimeUTC.datetime_to_timestamp(cur.fetchone()["jwt_iat"]),
|
||||
aud=f"plugin:{helper.get_stage_name()}" if for_plugin else f"front:{helper.get_stage_name()}"),
|
||||
aud=f"front:{helper.get_stage_name()}",
|
||||
exp=exp),
|
||||
"email": email,
|
||||
**r
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def create_sso_user(tenant_id, email, admin, name, origin, internal_id=None):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(f"""\
|
||||
WITH u AS (
|
||||
INSERT INTO public.users (tenant_id, email, role, name, data, origin, internal_id)
|
||||
VALUES (%(tenantId)s, %(email)s, %(role)s, %(name)s, %(data)s, %(origin)s, %(internal_id)s)
|
||||
RETURNING *
|
||||
)
|
||||
SELECT u.user_id AS id,
|
||||
u.email,
|
||||
u.role,
|
||||
u.name,
|
||||
TRUE AS change_password,
|
||||
(CASE WHEN u.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||
(CASE WHEN u.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||
(CASE WHEN u.role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
||||
u.appearance,
|
||||
origin
|
||||
FROM u;""",
|
||||
{"tenantId": tenant_id, "email": email, "internal_id": internal_id,
|
||||
"role": "admin" if admin else "member", "name": name, "origin": origin,
|
||||
"data": json.dumps({"lastAnnouncementView": TimeUTC.now()})})
|
||||
cur.execute(
|
||||
query
|
||||
)
|
||||
return helper.dict_to_camel_case(cur.fetchone())
|
||||
104
ee/api/chalicelib/utils/SAML2_helper.py
Normal file
104
ee/api/chalicelib/utils/SAML2_helper.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
from http import cookies
|
||||
from urllib.parse import urlparse, parse_qsl
|
||||
|
||||
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
||||
|
||||
from chalicelib.utils.helper import environ
|
||||
|
||||
SAML2 = {
|
||||
"strict": True,
|
||||
"debug": True,
|
||||
"sp": {
|
||||
"entityId": environ["SITE_URL"] + "/api/saml2/metadata/",
|
||||
"assertionConsumerService": {
|
||||
"url": environ["SITE_URL"] + "/api/saml2/acs",
|
||||
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||
},
|
||||
"singleLogoutService": {
|
||||
"url": environ["SITE_URL"] + "/api/saml2/sls",
|
||||
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
},
|
||||
"NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
"x509cert": "",
|
||||
"privateKey": ""
|
||||
},
|
||||
"idp": None
|
||||
}
|
||||
idp = None
|
||||
# SAML2 config handler
|
||||
if len(environ.get("SAML2_MD_URL")) > 0:
|
||||
print("SAML2_MD_URL provided, getting IdP metadata config")
|
||||
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
|
||||
|
||||
idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(environ.get("SAML2_MD_URL"))
|
||||
idp = idp_data.get("idp")
|
||||
|
||||
if SAML2["idp"] is None:
|
||||
if len(environ.get("idp_entityId", "")) > 0 \
|
||||
and len(environ.get("idp_sso_url", "")) > 0 \
|
||||
and len(environ.get("idp_x509cert", "")) > 0:
|
||||
idp = {
|
||||
"entityId": environ["idp_entityId"],
|
||||
"singleSignOnService": {
|
||||
"url": environ["idp_sso_url"],
|
||||
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
},
|
||||
"x509cert": environ["idp_x509cert"]
|
||||
}
|
||||
if len(environ.get("idp_sls_url", "")) > 0:
|
||||
idp["singleLogoutService"] = {
|
||||
"url": environ["idp_sls_url"],
|
||||
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
}
|
||||
|
||||
if idp is None:
|
||||
print("No SAML2 IdP configuration found")
|
||||
else:
|
||||
SAML2["idp"] = idp
|
||||
|
||||
|
||||
def init_saml_auth(req):
|
||||
# auth = OneLogin_Saml2_Auth(req, custom_base_path=environ['SAML_PATH'])
|
||||
|
||||
if idp is None:
|
||||
raise Exception("No SAML2 config provided")
|
||||
auth = OneLogin_Saml2_Auth(req, old_settings=SAML2)
|
||||
|
||||
return auth
|
||||
|
||||
|
||||
def prepare_request(request):
|
||||
request.args = dict(request.query_params).copy() if request.query_params else {}
|
||||
request.form = dict(request.json_body).copy() if request.json_body else dict(
|
||||
parse_qsl(request.raw_body.decode())) if request.raw_body else {}
|
||||
cookie_str = request.headers.get("cookie", "")
|
||||
if "session" in cookie_str:
|
||||
cookie = cookies.SimpleCookie()
|
||||
cookie.load(cookie_str)
|
||||
# Even though SimpleCookie is dictionary-like, it internally uses a Morsel object
|
||||
# which is incompatible with requests. Manually construct a dictionary instead.
|
||||
extracted_cookies = {}
|
||||
for key, morsel in cookie.items():
|
||||
extracted_cookies[key] = morsel.value
|
||||
session = extracted_cookies["session"]
|
||||
else:
|
||||
session = {}
|
||||
# If server is behind proxys or balancers use the HTTP_X_FORWARDED fields
|
||||
headers = request.headers
|
||||
url_data = urlparse('%s://%s' % (headers.get('x-forwarded-proto', 'http'), headers['host']))
|
||||
return {
|
||||
'https': 'on' if request.headers.get('x-forwarded-proto', 'http') == 'https' else 'off',
|
||||
'http_host': request.headers['host'],
|
||||
'server_port': url_data.port,
|
||||
'script_name': request.path,
|
||||
'get_data': request.args.copy(),
|
||||
# Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144
|
||||
# 'lowercase_urlencoding': True,
|
||||
'post_data': request.form.copy(),
|
||||
'cookie': {"session": session},
|
||||
'request': request
|
||||
}
|
||||
|
||||
|
||||
def is_saml2_available():
|
||||
return idp is not None
|
||||
|
|
@ -9,4 +9,5 @@ elasticsearch==7.9.1
|
|||
jira==2.0.0
|
||||
schedule==1.1.0
|
||||
croniter==1.0.12
|
||||
clickhouse-driver==0.1.5
|
||||
clickhouse-driver==0.1.5
|
||||
python3-saml==1.10.1
|
||||
|
|
@ -46,6 +46,7 @@ CREATE TABLE tenants
|
|||
);
|
||||
|
||||
CREATE TYPE user_role AS ENUM ('owner', 'admin', 'member');
|
||||
CREATE TYPE user_origin AS ENUM ('saml');
|
||||
CREATE TABLE users
|
||||
(
|
||||
user_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
|
|
@ -119,7 +120,9 @@ CREATE TABLE users
|
|||
api_key text UNIQUE default generate_api_key(20) not null,
|
||||
jwt_iat timestamp without time zone NULL DEFAULT NULL,
|
||||
data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
weekly_report boolean NOT NULL DEFAULT TRUE
|
||||
weekly_report boolean NOT NULL DEFAULT TRUE,
|
||||
origin user_origin NULL DEFAULT NULL,
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -66,4 +66,9 @@ env:
|
|||
# Enable logging for python app
|
||||
# Ref: https://stackoverflow.com/questions/43969743/logs-in-kubernetes-pod-not-showing-up
|
||||
PYTHONUNBUFFERED: '0'
|
||||
version_number: '1.0.0'
|
||||
version_number: '1.1.0'
|
||||
SAML2_MD_URL: ''
|
||||
idp_entityId: ''
|
||||
idp_sso_url: ''
|
||||
idp_x509cert: ''
|
||||
idp_sls_url: ''
|
||||
|
|
|
|||
|
|
@ -31,4 +31,8 @@ CREATE INDEX resources_session_id_timestamp_duration_durationgt0NN_img_idx ON ev
|
|||
CREATE INDEX resources_timestamp_session_id_idx ON events.resources (timestamp, session_id);
|
||||
CREATE INDEX errors_project_id_error_id_integration_idx ON public.errors (project_id, error_id) WHERE source != 'js_exception';
|
||||
CREATE INDEX errors_error_id_timestamp_session_id_idx ON events.errors (error_id, timestamp, session_id);
|
||||
CREATE TYPE user_origin AS ENUM ('saml');
|
||||
ALTER TABLE public.users
|
||||
ADD COLUMN origin user_origin NULL DEFAULT NULL,
|
||||
ADD COLUMN internal_id text NULL DEFAULT NULL;
|
||||
COMMIT;
|
||||
Loading…
Add table
Reference in a new issue