diff --git a/.gitignore b/.gitignore index 08577e3..4039205 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ stepup/gateway/surfnet_yubikey.yaml stepup/.env -/.idea \ No newline at end of file +/.idea +.env diff --git a/core/dbschema/createdbs.sql b/core/dbschema/createdbs.sql index 370f330..d65948b 100755 --- a/core/dbschema/createdbs.sql +++ b/core/dbschema/createdbs.sql @@ -6,6 +6,7 @@ CREATE DATABASE IF NOT EXISTS spdashboard; CREATE DATABASE IF NOT EXISTS invite; CREATE DATABASE IF NOT EXISTS userlifecycle; CREATE DATABASE IF NOT EXISTS spdashboard; +CREATE DATABASE IF NOT EXISTS sbs; CREATE USER IF NOT EXISTS 'ebrw'@'%' IDENTIFIED BY 'secret'; GRANT ALL PRIVILEGES ON eb.* TO 'ebrw'@'%'; @@ -30,3 +31,6 @@ GRANT ALL PRIVILEGES ON userlifecycle.* TO 'userlifecyclerw'@'%'; CREATE USER IF NOT EXISTS 'spdrwrw'@'%' IDENTIFIED BY 'secret'; GRANT ALL PRIVILEGES ON spdashboard.* TO 'spdrwrw'@'%'; + +CREATE USER IF NOT EXISTS 'sbs'@'%' IDENTIFIED BY 'secret'; +GRANT ALL PRIVILEGES ON sbs.* TO 'sbs'@'%'; diff --git a/core/docker-compose.yml b/core/docker-compose.yml index bff9a19..d34b4a3 100644 --- a/core/docker-compose.yml +++ b/core/docker-compose.yml @@ -21,6 +21,7 @@ services: - spdashboard.dev.openconext.local - mujina-idp.dev.openconext.local - invite.dev.openconext.local + - sbs.dev.openconext.local hostname: haproxy.docker mariadb: @@ -42,7 +43,6 @@ services: mongo: image: bitnami/mongodb:7.0 - restart: always environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: secret @@ -416,6 +416,55 @@ services: coreconextdev: hostname: mailcatcher.docker + redis: + image: "redis" + healthcheck: + test: ["CMD", "redis-cli","ping"] + timeout: 5s + retries: 10 + networks: + coreconextdev: + hostname: redis.docker + profiles: + - "sbs" + + # This is apache RP + # client -> docroot + # api -> sbs-server:8080 + sbs: + image: ghcr.io/surfscz/sram-sbs-client:openconext-dev + networks: + coreconextdev: + hostname: sbs.docker + depends_on: + sbs-server: + condition: service_healthy + profiles: + - "sbs" + + sbs-server: + image: ghcr.io/surfscz/sram-sbs-server:openconext-dev + environment: + TESTING: 1 + PROFILE: "local" + ALLOW_MOCK_USER_API: 1 + volumes: + - ./sbs/config:/opt/sbs/config + networks: + coreconextdev: + healthcheck: + test: ["CMD", "curl", "--fail", "-s", "http://localhost:8080/health"] + timeout: 5s + retries: 10 + hostname: sbs-server.docker + depends_on: + redis: + condition: service_healthy + mariadb: + condition: service_healthy + profiles: + - "sbs" + networks: coreconextdev: driver: bridge diff --git a/core/sbs/config/alembic.ini b/core/sbs/config/alembic.ini new file mode 100644 index 0000000..3613647 --- /dev/null +++ b/core/sbs/config/alembic.ini @@ -0,0 +1,73 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +#truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = mysql+mysqldb://sbs:secret@mariadb/sbs?charset=utf8mb4 + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = NOTSET +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = DEBUG +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/core/sbs/config/config.yml b/core/sbs/config/config.yml new file mode 100644 index 0000000..cbf3a8b --- /dev/null +++ b/core/sbs/config/config.yml @@ -0,0 +1,257 @@ +database: + uri: mysql+mysqldb://sbs:secret@mariadb/sbs?charset=utf8mb4 + +secret_key: geheim +# Must be a base64 encoded key of 128, 192, or 256 bits. +# Hint: base64.b64encode(AESGCM.generate_key(bit_length=256)).decode() or +# base64.b64encode(os.urandom(256 // 8)).decode() +encryption_key: 3Kw2sDznh4jSZsShUcsxgfeOkaaKE8TC24OWJ1KWeDs= + +# Lifetime of session in minutes (one day is 60 * 24) +permanent_session_lifetime: 60 + +redis: + uri: redis://redis:6379/ + +socket_url: 0.0.0.0:8080/ + +api_users: + - name: "sysadmin" + password: "secret" + scopes: ["read", "write", "system", "ipaddress"] + - name: "sysread" + password: "secret" + scopes: ["read"] + - name: "research_cloud" + password: "secret" + scopes: ["restricted_co"] + - name: "ipaddress" + password: "secret" + scopes: ["ipaddress"] + +oidc: + client_id: foo + client_secret: echtgeheim + audience: http://http://sbs.dev.openconext.local + authorization_endpoint: http://http://sbs.dev.openconext.local/saml2sp/OIDC/authorization + token_endpoint: http://http://sbs.dev.openconext.local/OIDC/token + userinfo_endpoint: http://http://sbs.dev.openconext.local/OIDC/userinfo + jwks_endpoint: http://http://sbs.dev.openconext.local/OIDC/jwks + #Note that the paths for these uri's is hardcoded and only domain and port differ per environment + redirect_uri: http://http://sbs.dev.openconext.local/api/users/resume-session + continue_eduteams_redirect_uri: http://sbs.dev.openconext.local/continue + second_factor_authentication_required: True + totp_token_name: "SRAM local" + # The client_id of SBS. Most likely to equal the oidc.client_id + sram_service_entity_id: http://sbs.dev.openconext.local + + + scopes: + - profile + - eduperson_scoped_affiliation + - voperson_external_affiliation + - email + - ssh_public_key + - eduperson_orcid + - uid + - voperson_external_id + - eduperson_entitlement + - eduperon_assurance + - openid + - eduperson_principal_name + - voperson_id + +base_scope: "test.sbs.local" +entitlement_group_namespace: "urn:example:sbs" +eppn_scope: "test.sram.surf.nl" +collaboration_creation_allowed_entitlement: "urn:example:sbs:allow-create-co" + +environment_disclaimer: "local" + +mail: + host: localhost + port: 1025 + sender_name: SURF_ResearchAccessManagement + sender_email: no-reply@surf.nl + suppress_sending_mails: False + info_email: sram-support@surf.nl + beheer_email: sram-beheer@surf.nl + ticket_email: sram-support@surf.nl + eduteams_email: support+sram@eduteams.org + # Do we mail a summary of new Organizations and Services to the beheer_email? + audit_trail_notifications_enabled: True + account_deletion_notifications_enabled: True + send_exceptions: False + send_js_exceptions: False + send_exceptions_recipients: ["sram-support@surf.nl"] + environment: local + +manage: + enabled: false + base_url: "" + user: "" + password: "" + +aup: + version: 1 + url_aup_en: https://edu.nl/6wb63 + url_aup_nl: https://edu.nl/6wb63 + +base_url: http://sbs.dev.openconext.local +base_server_url: http://sbs.dev.openconext.local +wiki_link: https://edu.nl/vw3jx + +admin_users: + - uid: "urn:john" + - uid: "urn:rocky" + - uid: "urn:mike" + - uid: "urn:john" + +organisation_categories: + - "Research" + - "University" + - "Medical" + +feature: + seed_allowed: True + api_keys_enabled: True + feedback_enabled: True + impersonation_allowed: True + sbs_swagger_enabled: True + admin_platform_backdoor_totp: True + past_dates_allowed: True + mock_scim_enabled: True + +# The retention config determines how long users may be inactive, how long the reminder magic link is valid and when do we resent the magic link +retention: + allowed_inactive_period_days: 365 + reminder_suspend_period_days: 7 + remove_suspended_users_period_days: 90 + reminder_expiry_period_days: 7 + cron_hour_of_day: 7 + admin_notification_mail: True + +metadata: + idp_url: https://metadata.surfconext.nl/idps-metadata.xml + parse_at_startup: False + +service_bus: + enabled: False + host: "localhost" + client_id: "sbs" + user: "sbs" + password: "changethispassword" + +# note: all cron hours below are in UTC +platform_admin_notifications: + # Do we daily check for CO join_requests and CO requests and send a summary mail to beheer_email? + enabled: True + cron_hour_of_day: 11 + # How long before we include open join_requests in the summary + outstanding_join_request_days_threshold: 7 + # How long before we include open CO requests in the summary + outstanding_coll_request_days_threshold: 7 + +user_requests_retention: + # Do we daily check for CO join_requests and CO requests and delete approved and denied? + enabled: True + cron_hour_of_day: 10 + # How long before we delete approved / denied join_requests + outstanding_join_request_days_threshold: 21 + # How long before we delete approved / denied CO requests + outstanding_coll_request_days_threshold: 21 + +collaboration_expiration: + # Do we daily check for CO's that will be deleted because they have been expired? + enabled: True + cron_hour_of_day: 10 + # How long after expiration do we actually delete expired collaborations + expired_collaborations_days_threshold: 90 + # How many days before actual expiration do we mail the organisation members + expired_warning_mail_days_threshold: 5 + +collaboration_suspension: + # Do we daily check for CO's that will be suspended because of inactivity? + enabled: True + cron_hour_of_day: 10 + # After how many days of inactivity do we suspend collaborations + collaboration_inactivity_days_threshold: 360 + # How many days before actual suspension do we mail the organisation members + inactivity_warning_mail_days_threshold: 5 + # After how many days after suspension do we actually delete the collaboration + collaboration_deletion_days_threshold: 90 + +membership_expiration: + # Do we daily check for memberships that will be deleted because they have been expired? + enabled: True + cron_hour_of_day: 10 + # How long after expiration do we actually delete expired memberships + expired_memberships_days_threshold: 90 + # How many days before actual expiration do we mail the co admin and member + expired_warning_mail_days_threshold: 14 + +invitation_reminders: + # Do we daily check for invitations that need a reminder? + enabled: True + cron_hour_of_day: 10 + # How long before expiration of an invitation do we remind the user? + invitation_reminders_threshold: 5 + +orphan_users: + # Do we daily check for users that are orphans? + enabled: True + cron_hour_of_day: 10 + # How long after created do we delete orphan users + delete_days_threshold: -1 + +open_requests: + # Do we weekly check for all open requests? + enabled: True + cron_day_of_week: 1 + +scim_sweep: + # Do we enable scim sweeps? + enabled: True + # How often do we check if scim sweeps are needed per service + cron_minutes_expression: "*/15" + +ldap: + url: ldap://localhost:1389/dc=example,dc=org + bind_account: cn=admin,dc=example,dc=org + +# A MFA login in a different flow is valid for X minutes +mfa_sso_time_in_minutes: 10 + +# whether to require TOTP for users form IdPs that match neither mfa_idp_allowed +# nor ssid_identity_providers +mfa_fallback_enabled: true + +# Lower case schachome organisations / entity ID's allowed skipping MFA; +# MFA is supposed to be handled at the IdP for these entities +mfa_idp_allowed: + - schac_home: "idp.test" + entity_id: "https://idp.test" + - schac_home: "erroridp.example.edu" + entity_id: "https://erroridp.example.edu" + - schac_home: "only_sho" + - entity_id: "https://only_entityid" + +# Lower case schachome organisations / entity ID's where SURFSecure ID is used for step-up +# If this feature is no longer needed, just replace the value with an empty list [] +ssid_identity_providers: + - schac_home: "ssid.org" + entity_id: "https://ssid.org" + - schac_home: "erroridp.example.edu" + entity_id: "https://erroridp.example.edu" + +ssid_config_folder: saml_test + +pam_web_sso: + session_timeout_seconds: 300 + +rate_limit_totp_guesses_per_30_seconds: 10 + +# The uid's of user that will never be suspended or deleted +excluded_user_accounts: + - uid: "urn:paul" + - uid: "urn:peter" diff --git a/core/sbs/config/disclaimer.css b/core/sbs/config/disclaimer.css new file mode 100644 index 0000000..e89bbfc --- /dev/null +++ b/core/sbs/config/disclaimer.css @@ -0,0 +1,6 @@ +{% if environment_name!="prd" -%} +body::after { + background: {{ sbs_disclaimer_color }}; + content: "{{ sbs_disclaimer_label }}"; +} +{% endif %} diff --git a/core/sbs/config/saml/saml_advanced_settings.json b/core/sbs/config/saml/saml_advanced_settings.json new file mode 100644 index 0000000..bdde320 --- /dev/null +++ b/core/sbs/config/saml/saml_advanced_settings.json @@ -0,0 +1,35 @@ +{ + "security": { + "nameIdEncrypted": false, + "authnRequestsSigned": true, + "logoutRequestSigned": false, + "logoutResponseSigned": false, + "signMetadata": false, + "wantMessagesSigned": false, + "wantAssertionsSigned": true, + "wantNameId" : true, + "wantNameIdEncrypted": false, + "wantAttributeStatement": false, + "wantAssertionsEncrypted": false, + "requestedAuthnContext": ["{{sbs_ssid_authncontext}}"], + "requestedAuthnContextComparison": "minimum", + "failOnAuthnContextMismatch": false, + "allowSingleLabelDomains": false, + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + "rejectDeprecatedAlgorithm": true + }, + "contactPerson": { + "technical": { + "givenName": "{{ mail.admin_name }}", + "emailAddress": "{{ mail.admin_address }}" + } + }, + "organization": { + "en-US": { + "name": "{{ org.name }}", + "displayname": "{{ org.name }}", + "url": "{{ org.url }}" + } + } +} diff --git a/core/sbs/config/saml/saml_settings.json b/core/sbs/config/saml/saml_settings.json new file mode 100644 index 0000000..bb5788e --- /dev/null +++ b/core/sbs/config/saml/saml_settings.json @@ -0,0 +1,22 @@ +{ + "strict": true, + "debug": true, + "sp": { + "entityId": "{{ sbs_surf_secure_id.sp_entity_id }}", + "assertionConsumerService": { + "url": "{{ sbs_surf_secure_id.acs_url }}", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "x509cert": "{{ sbs_surf_secure_id.pub | barepem }}", + "privateKey": "{{ sbs_surf_secure_id.priv | barepem }}" + }, + "idp": { + "entityId": "{{ sbs_ssid_entityid }}", + "singleSignOnService": { + "url": "{{ sbs_ssid_sso_endpoint }}", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + "x509cert": "{{ sbs_surf_secure_id.sa_idp_certificate | barepem }}" + } +} diff --git a/core/sbs/docker-compose.override.yml b/core/sbs/docker-compose.override.yml new file mode 100644 index 0000000..c70a7ef --- /dev/null +++ b/core/sbs/docker-compose.override.yml @@ -0,0 +1,59 @@ +--- +services: + sbs: + image: ghcr.io/surfscz/sram-sbs:openconext-dev + networks: + coreconextdev: + hostname: sbs.docker + depends_on: + sbs-client: + condition: service_healthy + sbs-server: + condition: service_healthy + profiles: + - "sbs" + + # This is the version with node/yarn + sbs-client: + image: node:22 + environment: + - NODE_ENV=development + - HOST=0.0.0.0 + - PORT=8080 + networks: + coreconextdev: + volumes: + - ${SBS_CODE_PATH}/client:/home/node/app + working_dir: /home/node/app + healthcheck: + test: ["CMD", "curl", "--fail", "-s", "http://localhost:8080/index.html"] + timeout: 5s + retries: 10 + hostname: sbs-client.docker + command: "yarn start" + profiles: + - "sbs" + + sbs-server: + image: ghcr.io/surfscz/sram-sbs-server:openconext-dev + environment: + TESTING: 1 + PROFILE: "local" + ALLOW_MOCK_USER_API: 1 + volumes: + - ./sbs/config:/opt/sbs/config + - ${SBS_CODE_PATH}/server:/opt/sbs/server + networks: + coreconextdev: + healthcheck: + test: ["CMD", "curl", "--fail", "-s", "http://localhost:8080/health"] + timeout: 5s + retries: 10 + hostname: sbs-server.docker + depends_on: + redis: + condition: service_healthy + mariadb: + condition: service_healthy + profiles: + - "sbs"