Compare commits
13
Commits
87a97a3ae4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f59d2b72fb
|
||
|
|
eea6389a5a
|
||
|
|
d694ae5650
|
||
|
|
ab6dbac3fd
|
||
|
|
47adc08243
|
||
|
|
099e8b1001
|
||
|
|
41dfb8d3d4
|
||
|
|
a56eec0b1c
|
||
|
|
4c1174df19
|
||
|
|
b64bb5683b
|
||
|
|
18a94f0eb0
|
||
|
|
827ec93663
|
||
|
|
e048f579a2
|
+9
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"python-envs.pythonProjects": [
|
||||||
|
{
|
||||||
|
"path": ".",
|
||||||
|
"envManager": "ms-python.python:system",
|
||||||
|
"packageManager": "ms-python.python:pip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ Defaults deliberately refuse incomplete membership deployments. Configure:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `www_namespace` | Namespace, default `www` |
|
| `www_namespace` | Namespace, default `www` |
|
||||||
| `www_hostname` | Canonical HTTPS hostname, default **www.eom.dev** |
|
| `www_hostname` | Canonical HTTPS hostname, default **www.eom.dev** |
|
||||||
|
| `www_redirect_hostnames` | Hostnames 301-redirected to `www_hostname`, default `[]` |
|
||||||
| `www_membership_enabled` | Deploy membership/proxy, default true |
|
| `www_membership_enabled` | Deploy membership/proxy, default true |
|
||||||
| `www_membership_image` | Required versioned application image/digest |
|
| `www_membership_image` | Required versioned application image/digest |
|
||||||
| `www_membership_secret_name` | Existing Secret, default `membership` |
|
| `www_membership_secret_name` | Existing Secret, default `membership` |
|
||||||
@@ -79,6 +80,14 @@ Stripe needs `STRIPE_PORTAL_CONFIGURATION`. SMTP STARTTLS, production PostgreSQL
|
|||||||
and HTTPS are required; production SQLite is rejected. A complete override of
|
and HTTPS are required; production SQLite is rejected. A complete override of
|
||||||
`www_membership_environment` is possible but must preserve mandatory defaults.
|
`www_membership_environment` is possible but must preserve mandatory defaults.
|
||||||
|
|
||||||
|
LDAP uses verified LDAPS/StartTLS by default. For deliberate plaintext LDAP
|
||||||
|
inside a trusted cluster, set `LDAP_URL: ldap://openldap.openldap.svc.cluster.local:389`
|
||||||
|
and `LDAP_ALLOW_INSECURE: "1"` in `www_membership_environment_overrides`, then
|
||||||
|
redeploy with a membership image containing this option. This applies to both web
|
||||||
|
and worker and sends bind/registration passwords and directory data unencrypted.
|
||||||
|
The default is `"0"`; `ldaps://` remains certificate-verified regardless of this
|
||||||
|
option. There is no automatic downgrade after a TLS failure.
|
||||||
|
|
||||||
Payment flags validate operator intent; **actual application availability is
|
Payment flags validate operator intent; **actual application availability is
|
||||||
controlled by its environment, secrets and tiers**. Disabling Stripe validation
|
controlled by its environment, secrets and tiers**. Disabling Stripe validation
|
||||||
alone does not remove its button. Set an empty tier mapping to disable all paid
|
alone does not remove its button. Set an empty tier mapping to disable all paid
|
||||||
@@ -86,10 +95,49 @@ tiers. SHKeeper needs both its flag and URL/currencies plus API key in the Secre
|
|||||||
It prepays one calendar month per invoice, without automatic renewal.
|
It prepays one calendar month per invoice, without automatic renewal.
|
||||||
|
|
||||||
Defaults reuse product `prod_Sc9N5J0vNAdLCn` and the same five live Price IDs as
|
Defaults reuse product `prod_Sc9N5J0vNAdLCn` and the same five live Price IDs as
|
||||||
the application. Provision dedicated `www-level-0` through `www-level-4`
|
the application. Level 0 is the free account baseline; paid offers are levels
|
||||||
|
1–5. Provision dedicated `www-level-1` through `www-level-5`
|
||||||
groupOfUniqueNames groups, sentinel and ACLs separately. Override group DNs if
|
groupOfUniqueNames groups, sentinel and ACLs separately. Override group DNs if
|
||||||
needed; sandbox testing requires separate test-mode prices.
|
needed; sandbox testing requires separate test-mode prices.
|
||||||
|
|
||||||
|
### Service access groups, including free Level 0
|
||||||
|
|
||||||
|
Set `www_membership_service_groups` to a mapping of **quoted effective level**
|
||||||
|
keys to lists of pre-created LDAP group DNs. The default `{}` grants no additional
|
||||||
|
groups. For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
www_membership_service_groups:
|
||||||
|
"0": ["cn=community-chat,ou=membership-services,dc=example,dc=org"]
|
||||||
|
"1": ["cn=community-chat,ou=membership-services,dc=example,dc=org"]
|
||||||
|
"2":
|
||||||
|
- "cn=community-chat,ou=membership-services,dc=example,dc=org"
|
||||||
|
- "cn=game-servers,ou=membership-services,dc=example,dc=org"
|
||||||
|
```
|
||||||
|
|
||||||
|
The highest valid paid membership selects exactly that level's list. **No implicit
|
||||||
|
inheritance**: include groups again for levels 3–5 if they should also receive
|
||||||
|
them. Level 0 is selected only when no paid entitlement remains; repeat a free
|
||||||
|
service DN at every level to offer it to all members. Original tier `group_dn`
|
||||||
|
grants are unchanged and independent of these additional service groups.
|
||||||
|
|
||||||
|
The role serializes the mapping into the existing membership-tiers ConfigMap as
|
||||||
|
`service-groups.json`, sets `MEMBERSHIP_SERVICE_GROUPS_FILE`, and includes it in
|
||||||
|
the configuration revision so updates trigger workload rollout. Do not override
|
||||||
|
it using `MEMBERSHIP_SERVICE_GROUPS_JSON` in the public environment. Publish an
|
||||||
|
updated membership image before deploying this role change; no new DB migration
|
||||||
|
is needed. See [tests/example-vars.yml](tests/example-vars.yml) for all six levels.
|
||||||
|
|
||||||
|
Targets must be dedicated `groupOfUniqueNames` groups with the existing sentinel;
|
||||||
|
provision groups and narrow `uniqueMember` worker ACLs separately. Configure each
|
||||||
|
service/Dex to authorize those groups. Never map admin/staff or independently
|
||||||
|
managed groups: the worker **removes noneligible local members**, including
|
||||||
|
manually granted memberships. Keep retired groups/ACLs until revocation completes.
|
||||||
|
Historical group targets persist for safe removal after mappings change. Expiry
|
||||||
|
returns users to level 0 without waiting for Stripe/SHKeeper, but LDAP outages
|
||||||
|
and downstream cached sessions can still delay access changes. Free grants cover
|
||||||
|
local membership accounts, not untouched users elsewhere in LDAP.
|
||||||
|
|
||||||
### Secrets
|
### Secrets
|
||||||
|
|
||||||
Create an Opaque Secret using External Secrets, Sealed Secrets or your secret
|
Create an Opaque Secret using External Secrets, Sealed Secrets or your secret
|
||||||
@@ -115,12 +163,66 @@ Do not use root/admin LDAP credentials. Restrict ACLs to the website account OU
|
|||||||
and dedicated group attributes. Use SHKeeper's invoice API key, never wallet
|
and dedicated group attributes. Use SHKeeper's invoice API key, never wallet
|
||||||
private keys or UI/payout credentials.
|
private keys or UI/payout credentials.
|
||||||
|
|
||||||
|
`LDAP_BIND_DN` names the membership service account; `LDAP_BIND_PASSWORD` must
|
||||||
|
be its original plaintext password, **not** its stored LDAP `userPassword` hash
|
||||||
|
or an already-base64-encoded value. Supply original values in
|
||||||
|
`www_membership_secret_data`; the role base64-encodes them once for Secret `data`.
|
||||||
|
Current membership login uses verified Dex issuer/subject claims without an LDAP
|
||||||
|
connection. These service credentials remain required for account registration
|
||||||
|
and worker group synchronization. Older images logging
|
||||||
|
`LDAP service bind rejected during Dex callback` or
|
||||||
|
`Dex callback rejected kind=LDAPInvalidCredentialsResult` still perform the
|
||||||
|
retired callback lookup; upgrade the membership image to remove that dependency.
|
||||||
|
Fix service credentials separately for registration/sync, then rerun the role to
|
||||||
|
roll out web and worker. Do not use directory administrator credentials.
|
||||||
|
`LDAP_PASSWORD_SCHEME` gates new registrations only; it does not select the hash
|
||||||
|
algorithm for existing-user login.
|
||||||
|
|
||||||
|
The Dex-only login release adds migration `0004_dex_login_identity`. Keep
|
||||||
|
`DEX_ISSUER` unchanged during this upgrade so existing subjects are pinned to
|
||||||
|
their original issuer, and coordinate replacing web/worker together. Existing
|
||||||
|
subject bindings and billing data are retained. Unbound local accounts require
|
||||||
|
proof from their recent registration session or an administrator's explicit
|
||||||
|
`bind_dex_identity` mapping (`--account`, `--uid`, `--subject`); they are never
|
||||||
|
claimed by email/username matching alone. Review any retired connector bindings
|
||||||
|
before deployment. Keep Dex's existing connector/uid mapping unchanged and do not
|
||||||
|
recycle usernames. The worker pins new users' directory UUIDs separately; LDAP
|
||||||
|
outages can still delay paid group grants/revocations even though login works.
|
||||||
|
|
||||||
Private trust roots belong in a separate CA Secret. Set paths such as
|
Private trust roots belong in a separate CA Secret. Set paths such as
|
||||||
`LDAP_CA_CERT_FILE`, `SHKEEPER_CA_CERT_FILE`, `PGSSLROOTCERT` or
|
`LDAP_CA_CERT_FILE`, `SHKEEPER_CA_CERT_FILE`, `PGSSLROOTCERT` or
|
||||||
`REQUESTS_CA_BUNDLE` to `/etc/membership/ca/<secret-key>`. The read-only mount is
|
`REQUESTS_CA_BUNDLE` to `/etc/membership/ca/<secret-key>`. The read-only mount is
|
||||||
shared by web, worker and migrations. Verify certificate expiry, strong keys,
|
shared by web, worker and migrations. Verify certificate expiry, strong keys,
|
||||||
SHA-2 signatures and trust independently; do not disable TLS verification.
|
SHA-2 signatures and trust independently; do not disable TLS verification.
|
||||||
|
|
||||||
|
### Self-service profiles
|
||||||
|
|
||||||
|
The profile release requires a membership image with Pillow and migration
|
||||||
|
`0005_profile_email_change`, plus the updated website templates/CSS. The role's
|
||||||
|
migration Job still runs before workloads. Deploy matching reviewed versions of
|
||||||
|
the application and website; do not reuse cached image tags.
|
||||||
|
|
||||||
|
The membership page links to an authenticated editor with friendly labels for
|
||||||
|
the requested inetOrgPerson attributes, with `displayName` labelled **Gamer tag**.
|
||||||
|
Dex's normal claims do not contain the full profile; opening the editor reads
|
||||||
|
the current LDAP entry, while ordinary sign-in remains LDAP-independent. The
|
||||||
|
service identity needs read/search access to profile attributes and operational
|
||||||
|
UUID/timestamp fields. Writes use the member's current password and **user bind**,
|
||||||
|
not broader service privileges. Configure narrow self-write ACLs, server-side
|
||||||
|
mail/uid uniqueness and RFC 4528 assertion support. SSH key edits additionally
|
||||||
|
require the `ldapPublicKey` schema and pre-added auxiliary class or an ACL allowing
|
||||||
|
only that specific self-add. This role does not create schema/ACLs automatically.
|
||||||
|
|
||||||
|
Email changes require mailbox verification plus the current password and replace
|
||||||
|
all existing mail values. Password hashing is delegated to slapd's Password Modify
|
||||||
|
policy; the application does not generate MD5-Crypt or expose stored hashes.
|
||||||
|
Review legacy hashing separately. See the application's self-service profile
|
||||||
|
documentation for the full attribute mapping and directory prerequisites.
|
||||||
|
|
||||||
|
The ingress upload limit is 2 MiB to accommodate a 1 MiB JPEG and multipart
|
||||||
|
overhead. Apache retains 256 KiB for non-profile member routes. Keep access/body
|
||||||
|
logging disabled for verification URLs, passwords and other profile data.
|
||||||
|
|
||||||
### Example inventory and hostname
|
### Example inventory and hostname
|
||||||
|
|
||||||
[tests/example-vars.yml](tests/example-vars.yml) contains public configuration
|
[tests/example-vars.yml](tests/example-vars.yml) contains public configuration
|
||||||
@@ -138,7 +240,10 @@ app defaults to eom.dev. This role sets `MEMBERSHIP_PUBLIC_URL` to
|
|||||||
|
|
||||||
To serve the apex instead, set `www_hostname: eom.dev` and align DNS,
|
To serve the apex instead, set `www_hostname: eom.dev` and align DNS,
|
||||||
certificates, callbacks and redirects. Membership requires one canonical HTTPS
|
certificates, callbacks and redirects. Membership requires one canonical HTTPS
|
||||||
origin for sessions, CSRF, email verification and OIDC.
|
origin for sessions, CSRF, email verification and OIDC. To send apex visitors
|
||||||
|
to the canonical origin without serving it there, set
|
||||||
|
`www_redirect_hostnames: [eom.dev]`; other ingresses' more specific apex paths
|
||||||
|
(such as `/.well-known/matrix/*`) keep working.
|
||||||
|
|
||||||
## Upgrades and operations
|
## Upgrades and operations
|
||||||
|
|
||||||
|
|||||||
+114
-17
@@ -15,6 +15,8 @@ www_repo_url: https://git.example.com/organization/www.git
|
|||||||
www_namespace: www
|
www_namespace: www
|
||||||
www_hostname: www.example.com
|
www_hostname: www.example.com
|
||||||
www_ingress_class: nginx
|
www_ingress_class: nginx
|
||||||
|
# Extra hostnames 301-redirected to https://{{ www_hostname }} (e.g. [eom.dev]).
|
||||||
|
www_redirect_hostnames: []
|
||||||
www_cluster_issuer: letsencrypt-prod
|
www_cluster_issuer: letsencrypt-prod
|
||||||
# Set to your published website image, including any required metrics exporter.
|
# Set to your published website image, including any required metrics exporter.
|
||||||
www_httpd_image: registry.example.com/organization/www:release
|
www_httpd_image: registry.example.com/organization/www:release
|
||||||
@@ -55,12 +57,54 @@ www_membership_stripe_enabled: false
|
|||||||
www_membership_shkeeper_enabled: false
|
www_membership_shkeeper_enabled: false
|
||||||
www_membership_environment_overrides: {}
|
www_membership_environment_overrides: {}
|
||||||
|
|
||||||
|
# Explicit effective membership level -> service group DNs. No inheritance.
|
||||||
|
# Include a group at each level that should receive it; level 0 is free-only.
|
||||||
|
# Only dedicate groups whose entire membership may be managed by this service.
|
||||||
|
www_membership_service_groups:
|
||||||
|
"0":
|
||||||
|
- "cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev"
|
||||||
|
"1":
|
||||||
|
- "cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev"
|
||||||
|
"2":
|
||||||
|
- "cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev"
|
||||||
|
"3":
|
||||||
|
- "cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Mastodon Users,ou=Mastodon,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=PeerTube Users,ou=PeerTube,ou=Services,dc=eom,dc=dev"
|
||||||
|
"4":
|
||||||
|
- "cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Mastodon Users,ou=Mastodon,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=PeerTube Users,ou=PeerTube,ou=Services,dc=eom,dc=dev"
|
||||||
|
"5":
|
||||||
|
- "cn=Dex Users,ou=Dex,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Discourse Users,ou=Discourse,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Gitea Users,ou=Gitea,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Matrix Users,ou=Matrix,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=Mastodon Users,ou=Mastodon,ou=Services,dc=eom,dc=dev"
|
||||||
|
- "cn=PeerTube Users,ou=PeerTube,ou=Services,dc=eom,dc=dev"
|
||||||
|
|
||||||
# Public settings only. Credentials belong in the membership Secret.
|
# Public settings only. Credentials belong in the membership Secret.
|
||||||
www_membership_environment:
|
www_membership_environment:
|
||||||
MEMBERSHIP_PUBLIC_URL: "https://{{ www_hostname }}"
|
MEMBERSHIP_PUBLIC_URL: "https://{{ www_hostname }}"
|
||||||
MEMBERSHIP_ALLOW_HTTP: "0"
|
MEMBERSHIP_ALLOW_HTTP: "0"
|
||||||
MEMBERSHIP_TRUST_PROXY: "1"
|
MEMBERSHIP_TRUST_PROXY: "1"
|
||||||
MEMBERSHIP_TIERS_FILE: /etc/membership/tiers/tiers.json
|
MEMBERSHIP_TIERS_FILE: /etc/membership/tiers/tiers.json
|
||||||
|
MEMBERSHIP_SERVICE_GROUPS_FILE: /etc/membership/tiers/service-groups.json
|
||||||
POSTGRES_HOST: membership-db
|
POSTGRES_HOST: membership-db
|
||||||
POSTGRES_PORT: "5432"
|
POSTGRES_PORT: "5432"
|
||||||
POSTGRES_DB: membership
|
POSTGRES_DB: membership
|
||||||
@@ -68,8 +112,11 @@ www_membership_environment:
|
|||||||
POSTGRES_SSLMODE: prefer
|
POSTGRES_SSLMODE: prefer
|
||||||
DEX_ISSUER: https://dex.example.com
|
DEX_ISSUER: https://dex.example.com
|
||||||
DEX_CLIENT_ID: www-membership
|
DEX_CLIENT_ID: www-membership
|
||||||
DEX_CONNECTOR_ID: www-ldap
|
DEX_CONNECTOR_ID: ldap
|
||||||
LDAP_URL: ldaps://ldap.example.com
|
LDAP_URL: ldaps://ldap.example.com
|
||||||
|
LDAP_ALLOW_INSECURE: "0"
|
||||||
|
# Used only for registration/group sync, not Dex login callbacks.
|
||||||
|
# Its plaintext bind password belongs in Secret LDAP_BIND_PASSWORD, not here.
|
||||||
LDAP_BIND_DN: ""
|
LDAP_BIND_DN: ""
|
||||||
LDAP_ACCOUNT_BASE_DN: ou=www,ou=people,dc=example,dc=com
|
LDAP_ACCOUNT_BASE_DN: ou=www,ou=people,dc=example,dc=com
|
||||||
LDAP_SEARCH_BASE_DN: ou=people,dc=example,dc=com
|
LDAP_SEARCH_BASE_DN: ou=people,dc=example,dc=com
|
||||||
@@ -86,37 +133,87 @@ www_membership_environment:
|
|||||||
SHKEEPER_URL: ""
|
SHKEEPER_URL: ""
|
||||||
SHKEEPER_CURRENCIES: ""
|
SHKEEPER_CURRENCIES: ""
|
||||||
SHKEEPER_CA_CERT_FILE: ""
|
SHKEEPER_CA_CERT_FILE: ""
|
||||||
|
# Newsletter opt-in at signup and on the profile. Enabled only when the
|
||||||
|
# LISTMONK_API_TOKEN Secret key is also present.
|
||||||
|
LISTMONK_URL: ""
|
||||||
|
LISTMONK_API_USER: ""
|
||||||
|
LISTMONK_LIST_NAME: eom.dev
|
||||||
|
|
||||||
# Existing LIVE Stripe prices. Provision the dedicated group DNs separately.
|
# Existing LIVE Stripe prices. Level 0 is free and is not configured as a paid tier.
|
||||||
|
# Provision the dedicated paid-group DNs separately.
|
||||||
# Override with {} to disable paid tiers, or test-mode prices for a sandbox.
|
# Override with {} to disable paid tiers, or test-mode prices for a sandbox.
|
||||||
www_membership_tiers:
|
www_membership_tiers:
|
||||||
level-0:
|
level-1:
|
||||||
label: Level 0
|
level: 1
|
||||||
|
label: Bronze
|
||||||
price_id: price_1Rgv4xC5KPXqTvVLfMd6z7wn
|
price_id: price_1Rgv4xC5KPXqTvVLfMd6z7wn
|
||||||
price_label: $4 USD / month
|
price_label: $4 USD / month
|
||||||
usd_amount: "4.00"
|
usd_amount: "4.00"
|
||||||
group_dn: cn=www-level-0,ou=groups,dc=eom,dc=dev
|
group_dn: cn=Level 1,ou=Memberships,dc=eom,dc=dev
|
||||||
level-1:
|
description: Kick off the experiment with a private place to connect and a closer look at what is being made.
|
||||||
label: Level 1
|
# Display benefits only; exclusive-content permissions are managed manually.
|
||||||
|
benefits:
|
||||||
|
- Exclusive members-only content on Discourse and Matrix
|
||||||
|
- Behind-the-scenes notes and work-in-progress updates
|
||||||
|
- Giveaways exclusive to Bronze members
|
||||||
|
- Optional credit on the community supporters page
|
||||||
|
level-2:
|
||||||
|
level: 2
|
||||||
|
label: Silver
|
||||||
price_id: price_1Rgv5QC5KPXqTvVLv1PaHWb5
|
price_id: price_1Rgv5QC5KPXqTvVLv1PaHWb5
|
||||||
price_label: $8 USD / month
|
price_label: $8 USD / month
|
||||||
usd_amount: "8.00"
|
usd_amount: "8.00"
|
||||||
group_dn: cn=www-level-1,ou=groups,dc=eom,dc=dev
|
group_dn: cn=Level 2,ou=Memberships,dc=eom,dc=dev
|
||||||
level-2:
|
description: Venture behind the scenes and explore new experiments before their public release.
|
||||||
label: Level 2
|
benefits:
|
||||||
|
- Bronze content and community benefits
|
||||||
|
- Giveaways exclusive to Silver members
|
||||||
|
- Early previews of music, games, videos and experiments when available
|
||||||
|
- Invitations to member playtests and listening sessions
|
||||||
|
level-3:
|
||||||
|
level: 3
|
||||||
|
label: Gold
|
||||||
price_id: price_1Rgv5rC5KPXqTvVL8Ol8sMUf
|
price_id: price_1Rgv5rC5KPXqTvVL8Ol8sMUf
|
||||||
price_label: $16 USD / month
|
price_label: $16 USD / month
|
||||||
usd_amount: "16.00"
|
usd_amount: "16.00"
|
||||||
group_dn: cn=www-level-2,ou=groups,dc=eom,dc=dev
|
group_dn: cn=Level 3,ou=Memberships,dc=eom,dc=dev
|
||||||
level-3:
|
description: Bring your perspective and help shape the next community experiment.
|
||||||
label: Level 3
|
benefits:
|
||||||
|
- Silver content and community benefits
|
||||||
|
- Giveaways exclusive to Gold members
|
||||||
|
- Member polls on future project topics and experiments
|
||||||
|
- Invitations to occasional small-group feedback sessions
|
||||||
|
level-4:
|
||||||
|
level: 4
|
||||||
|
label: Platinum
|
||||||
price_id: price_1Rgv6FC5KPXqTvVL39mAxYjm
|
price_id: price_1Rgv6FC5KPXqTvVL39mAxYjm
|
||||||
price_label: $32 USD / month
|
price_label: $32 USD / month
|
||||||
usd_amount: "32.00"
|
usd_amount: "32.00"
|
||||||
group_dn: cn=www-level-3,ou=groups,dc=eom,dc=dev
|
group_dn: cn=Level 4,ou=Memberships,dc=eom,dc=dev
|
||||||
level-4:
|
description: Support the platform and help guide its direction through exclusive polls.
|
||||||
label: Level 4
|
benefits:
|
||||||
|
- Gold content and community benefits
|
||||||
|
- Giveaways exclusive to Platinum members
|
||||||
|
- Exclusive polls on the direction of the platform
|
||||||
|
- A larger contribution toward servers and public projects
|
||||||
|
- Choose this level if you would like to give more
|
||||||
|
level-5:
|
||||||
|
level: 5
|
||||||
|
label: Diamond
|
||||||
price_id: price_1Rgv6mC5KPXqTvVLkcmznWZa
|
price_id: price_1Rgv6mC5KPXqTvVLkcmznWZa
|
||||||
price_label: $64 USD / month
|
price_label: $64 USD / month
|
||||||
usd_amount: "64.00"
|
usd_amount: "64.00"
|
||||||
group_dn: cn=www-level-4,ou=groups,dc=eom,dc=dev
|
group_dn: cn=Level 5,ou=Memberships,dc=eom,dc=dev
|
||||||
|
description: Help underwrite the public work and the community around it.
|
||||||
|
benefits:
|
||||||
|
- Gold content and community benefits
|
||||||
|
- Giveaways exclusive to Diamond members
|
||||||
|
- Exclusive polls on the direction of the platform
|
||||||
|
- Optional Diamond credit on the supporters page
|
||||||
|
- Your biggest boost for independent public projects
|
||||||
|
|
||||||
|
# Create any missing tier/service groups (with the placeholder member) in the
|
||||||
|
# OpenLDAP pod. Existing groups and their members are left untouched.
|
||||||
|
www_membership_ldap_ensure_groups: false
|
||||||
|
www_membership_ldap_namespace: openldap
|
||||||
|
www_membership_ldap_pod_selector: app=openldap
|
||||||
|
|||||||
+44
-2
@@ -13,10 +13,13 @@
|
|||||||
- "www_membership_image is search('(:[^/]+$|@sha256:[a-f0-9]{64}$)')"
|
- "www_membership_image is search('(:[^/]+$|@sha256:[a-f0-9]{64}$)')"
|
||||||
- www_membership_environment is mapping
|
- www_membership_environment is mapping
|
||||||
- www_membership_tiers is mapping
|
- www_membership_tiers is mapping
|
||||||
|
- www_membership_service_groups is mapping
|
||||||
- www_membership_environment.MEMBERSHIP_PUBLIC_URL == 'https://' ~ www_hostname
|
- www_membership_environment.MEMBERSHIP_PUBLIC_URL == 'https://' ~ www_hostname
|
||||||
- www_membership_environment.MEMBERSHIP_ALLOW_HTTP | string == '0'
|
- www_membership_environment.MEMBERSHIP_ALLOW_HTTP | string == '0'
|
||||||
- www_membership_environment.MEMBERSHIP_TRUST_PROXY | string == '1'
|
- www_membership_environment.MEMBERSHIP_TRUST_PROXY | string == '1'
|
||||||
- www_membership_environment.MEMBERSHIP_TIERS_FILE == '/etc/membership/tiers/tiers.json'
|
- www_membership_environment.MEMBERSHIP_TIERS_FILE == '/etc/membership/tiers/tiers.json'
|
||||||
|
- www_membership_environment.MEMBERSHIP_SERVICE_GROUPS_FILE == '/etc/membership/tiers/service-groups.json'
|
||||||
|
- "'MEMBERSHIP_SERVICE_GROUPS_JSON' not in www_membership_environment"
|
||||||
- "'MEMBERSHIP_SQLITE_PATH' not in www_membership_environment"
|
- "'MEMBERSHIP_SQLITE_PATH' not in www_membership_environment"
|
||||||
- "'MEMBERSHIP_TIERS_JSON' not in www_membership_environment"
|
- "'MEMBERSHIP_TIERS_JSON' not in www_membership_environment"
|
||||||
- www_membership_replicas | int >= 1
|
- www_membership_replicas | int >= 1
|
||||||
@@ -27,14 +30,27 @@
|
|||||||
- www_membership_environment.EMAIL_USE_TLS | string == '1'
|
- www_membership_environment.EMAIL_USE_TLS | string == '1'
|
||||||
- www_membership_environment.DEX_ISSUER is match('^https://[^/@]+')
|
- www_membership_environment.DEX_ISSUER is match('^https://[^/@]+')
|
||||||
- www_membership_environment.LDAP_URL is match('^ldaps?://[^/@]+')
|
- www_membership_environment.LDAP_URL is match('^ldaps?://[^/@]+')
|
||||||
|
- (www_membership_environment.LDAP_ALLOW_INSECURE | default('0') | string) in ['0', '1']
|
||||||
- not www_membership_shkeeper_enabled or (www_membership_environment.SHKEEPER_URL is match('^https://[^/@]+') and www_membership_environment.SHKEEPER_CURRENCIES | length > 0)
|
- not www_membership_shkeeper_enabled or (www_membership_environment.SHKEEPER_URL is match('^https://[^/@]+') and www_membership_environment.SHKEEPER_CURRENCIES | length > 0)
|
||||||
- www_membership_shkeeper_enabled or (www_membership_environment.SHKEEPER_URL | length == 0 and www_membership_environment.SHKEEPER_CURRENCIES | length == 0)
|
- www_membership_shkeeper_enabled or (www_membership_environment.SHKEEPER_URL | length == 0 and www_membership_environment.SHKEEPER_CURRENCIES | length == 0)
|
||||||
- not www_membership_stripe_enabled or www_membership_environment.STRIPE_PORTAL_CONFIGURATION | length > 0
|
- not www_membership_stripe_enabled or www_membership_environment.STRIPE_PORTAL_CONFIGURATION | length > 0
|
||||||
|
- (www_membership_environment.LISTMONK_URL | default('')) | length == 0 or www_membership_environment.LISTMONK_URL is match('^https://[^/@]+$')
|
||||||
- not www_membership_postgres_managed or www_membership_environment.POSTGRES_HOST == 'membership-db'
|
- not www_membership_postgres_managed or www_membership_environment.POSTGRES_HOST == 'membership-db'
|
||||||
- www_membership_postgres_managed or www_membership_environment.POSTGRES_SSLMODE == 'verify-full'
|
- www_membership_postgres_managed or www_membership_environment.POSTGRES_SSLMODE == 'verify-full'
|
||||||
- not www_membership_postgres_managed or www_membership_environment.POSTGRES_PORT | string == '5432'
|
- not www_membership_postgres_managed or www_membership_environment.POSTGRES_PORT | string == '5432'
|
||||||
fail_msg: Supply a versioned membership image and configure verified LDAP, SMTP, Dex, database and payment settings (see role README).
|
fail_msg: Supply a versioned membership image and configure verified LDAP, SMTP, Dex, database and payment settings (see role README).
|
||||||
|
|
||||||
|
- name: Validate service group level mappings
|
||||||
|
tags: [membership_validate]
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- item.key | string is match('^(0|[1-9][0-9]*)$')
|
||||||
|
- item.key | string in (['0'] + (www_membership_tiers.values() | map(attribute='level') | map('string') | list))
|
||||||
|
- item.value is sequence and item.value is not string and item.value is not mapping
|
||||||
|
- item.value | select('string') | list | length == item.value | length
|
||||||
|
fail_msg: Service groups must map explicit configured levels (including 0) to lists of LDAP group DNs.
|
||||||
|
loop: "{{ www_membership_service_groups | dict2items }}"
|
||||||
|
|
||||||
- name: Validate optional CA file mount paths
|
- name: Validate optional CA file mount paths
|
||||||
tags: [membership_validate]
|
tags: [membership_validate]
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
@@ -46,7 +62,7 @@
|
|||||||
- name: Define secret-only settings
|
- name: Define secret-only settings
|
||||||
tags: [membership_validate]
|
tags: [membership_validate]
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
_www_secret_keys: [MEMBERSHIP_SECRET_KEY, POSTGRES_PASSWORD, DEX_CLIENT_SECRET, LDAP_BIND_PASSWORD, EMAIL_HOST_PASSWORD, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SHKEEPER_API_KEY]
|
_www_secret_keys: [MEMBERSHIP_SECRET_KEY, POSTGRES_PASSWORD, DEX_CLIENT_SECRET, LDAP_BIND_PASSWORD, EMAIL_HOST_PASSWORD, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SHKEEPER_API_KEY, LISTMONK_API_TOKEN]
|
||||||
_www_required_secret_keys: "{{ ['MEMBERSHIP_SECRET_KEY', 'POSTGRES_PASSWORD', 'DEX_CLIENT_SECRET', 'LDAP_BIND_PASSWORD'] + (['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'] if www_membership_stripe_enabled else []) + (['SHKEEPER_API_KEY'] if www_membership_shkeeper_enabled else []) }}"
|
_www_required_secret_keys: "{{ ['MEMBERSHIP_SECRET_KEY', 'POSTGRES_PASSWORD', 'DEX_CLIENT_SECRET', 'LDAP_BIND_PASSWORD'] + (['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'] if www_membership_stripe_enabled else []) + (['SHKEEPER_API_KEY'] if www_membership_shkeeper_enabled else []) }}"
|
||||||
|
|
||||||
- name: Prevent secrets in public ConfigMaps
|
- name: Prevent secrets in public ConfigMaps
|
||||||
@@ -136,7 +152,7 @@
|
|||||||
- name: Compute configuration revision without secret contents
|
- name: Compute configuration revision without secret contents
|
||||||
# resourceVersion changes trigger a rollout without exposing secret values.
|
# resourceVersion changes trigger a rollout without exposing secret values.
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
_www_membership_revision: "{{ {'image': www_membership_image, 'environment': www_membership_environment, 'tiers': www_membership_tiers, 'secret_name': www_membership_secret_name, 'secret_version': (_www_membership_secret.resources[0].metadata.resourceVersion if not ansible_check_mode else 'check'), 'ca_name': www_membership_ca_secret_name, 'ca_version': (_www_membership_ca.resources[0].metadata.resourceVersion if www_membership_ca_secret_name | length > 0 and not ansible_check_mode else ''), 'rollout': www_rollout_revision} | to_json(sort_keys=True) | hash('sha256') }}"
|
_www_membership_revision: "{{ {'image': www_membership_image, 'environment': www_membership_environment, 'tiers': www_membership_tiers, 'service_groups': www_membership_service_groups, 'secret_name': www_membership_secret_name, 'secret_version': (_www_membership_secret.resources[0].metadata.resourceVersion if not ansible_check_mode else 'check'), 'ca_name': www_membership_ca_secret_name, 'ca_version': (_www_membership_ca.resources[0].metadata.resourceVersion if www_membership_ca_secret_name | length > 0 and not ansible_check_mode else ''), 'rollout': www_rollout_revision} | to_json(sort_keys=True) | hash('sha256') }}"
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Deploy public membership configuration and network isolation
|
- name: Deploy public membership configuration and network isolation
|
||||||
@@ -192,3 +208,29 @@
|
|||||||
status: "True"
|
status: "True"
|
||||||
loop: [membership, membership-worker]
|
loop: [membership, membership-worker]
|
||||||
when: not ansible_check_mode
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Find the OpenLDAP pod
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
kind: Pod
|
||||||
|
namespace: "{{ www_membership_ldap_namespace }}"
|
||||||
|
label_selectors: ["{{ www_membership_ldap_pod_selector }}"]
|
||||||
|
field_selectors: [status.phase=Running]
|
||||||
|
register: www_ldap_pods
|
||||||
|
when: www_membership_ldap_ensure_groups | bool
|
||||||
|
|
||||||
|
- name: Ensure membership tier and service groups exist in LDAP
|
||||||
|
kubernetes.core.k8s_exec:
|
||||||
|
namespace: "{{ www_membership_ldap_namespace }}"
|
||||||
|
pod: "{{ www_ldap_pods.resources[0].metadata.name }}"
|
||||||
|
command: >-
|
||||||
|
sh -c 'printf "%s\n" "dn: {{ item }}" "objectClass: groupOfUniqueNames"
|
||||||
|
"cn: {{ (item | split(","))[0] | regex_replace("^cn=", "") }}"
|
||||||
|
"uniqueMember: {{ (www_membership_environment | combine(www_membership_environment_overrides)).LDAP_GROUP_PLACEHOLDER_DN }}"
|
||||||
|
| ldapadd -x -H ldap://localhost -D "cn=admin,dc=eom,dc=dev" -w "$LDAP_ADMIN_PASSWORD"; rc=$?;
|
||||||
|
if [ $rc -eq 0 ]; then echo GROUP_CREATED; elif [ $rc -ne 68 ]; then exit $rc; fi'
|
||||||
|
loop: >-
|
||||||
|
{{ ((www_membership_tiers.values() | map(attribute='group_dn') | list)
|
||||||
|
+ (www_membership_service_groups.values() | flatten)) | unique }}
|
||||||
|
register: www_ldap_groups
|
||||||
|
changed_when: "'GROUP_CREATED' in (www_ldap_groups.stdout | default(''))"
|
||||||
|
when: www_membership_ldap_ensure_groups | bool
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ metadata:
|
|||||||
namespace: {{ www_namespace }}
|
namespace: {{ www_namespace }}
|
||||||
data:
|
data:
|
||||||
tiers.json: {{ www_membership_tiers | to_json | to_json }}
|
tiers.json: {{ www_membership_tiers | to_json | to_json }}
|
||||||
|
service-groups.json: {{ www_membership_service_groups | to_json | to_json }}
|
||||||
---
|
---
|
||||||
# Only Apache may assert the trusted proxy headers to the membership service.
|
# Only Apache may assert the trusted proxy headers to the membership service.
|
||||||
# Requires a CNI which actually enforces NetworkPolicy.
|
# Requires a CNI which actually enforces NetworkPolicy.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{% if www_membership_enabled %}
|
{% if www_membership_enabled %}
|
||||||
ProxyRequests Off
|
ProxyRequests Off
|
||||||
ProxyPreserveHost On
|
ProxyPreserveHost On
|
||||||
|
# Service sign-up buttons link to /members; the app lives under /members/.
|
||||||
|
# Absolute target: TLS ends at ingress, so a relative one becomes http://...:443
|
||||||
|
RedirectMatch 301 "^/members$" "https://{{ www_hostname }}/members/"
|
||||||
ProxyPass "/members/" "http://membership:8000/members/" connectiontimeout=5 timeout=120
|
ProxyPass "/members/" "http://membership:8000/members/" connectiontimeout=5 timeout=120
|
||||||
ProxyPassReverse "/members/" "http://membership:8000/members/"
|
ProxyPassReverse "/members/" "http://membership:8000/members/"
|
||||||
<Location "/members/">
|
<Location "/members/">
|
||||||
@@ -14,6 +17,10 @@ ProxyPassReverse "/members/" "http://membership:8000/members/"
|
|||||||
Require all denied
|
Require all denied
|
||||||
</If>
|
</If>
|
||||||
</Location>
|
</Location>
|
||||||
|
<Location "/members/profile/">
|
||||||
|
# Bounded JPEG upload plus multipart overhead; other routes remain 256 KiB.
|
||||||
|
LimitRequestBody 2097152
|
||||||
|
</Location>
|
||||||
{% else %}
|
{% else %}
|
||||||
# Membership proxy disabled. Existing stateful workloads are not deleted.
|
# Membership proxy disabled. Existing stateful workloads are not deleted.
|
||||||
<Location "/members/">
|
<Location "/members/">
|
||||||
|
|||||||
@@ -152,7 +152,8 @@ metadata:
|
|||||||
# Redact email verification tokens and OIDC codes at the edge, not only in
|
# Redact email verification tokens and OIDC codes at the edge, not only in
|
||||||
# Apache/Gunicorn. Do not enable request-body/error tracing for /members/.
|
# Apache/Gunicorn. Do not enable request-body/error tracing for /members/.
|
||||||
nginx.ingress.kubernetes.io/enable-access-log: "false"
|
nginx.ingress.kubernetes.io/enable-access-log: "false"
|
||||||
nginx.ingress.kubernetes.io/proxy-body-size: "256k"
|
# Apache retains 256 KiB for non-profile member routes.
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "2m"
|
||||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: {{ www_ingress_class }}
|
ingressClassName: {{ www_ingress_class }}
|
||||||
@@ -170,3 +171,39 @@ spec:
|
|||||||
tls:
|
tls:
|
||||||
- hosts: [{{ www_hostname }}]
|
- hosts: [{{ www_hostname }}]
|
||||||
secretName: www
|
secretName: www
|
||||||
|
{% if www_redirect_hostnames | length > 0 %}
|
||||||
|
---
|
||||||
|
# Send browsers on alias hostnames (e.g. the apex, whose /.well-known/matrix/*
|
||||||
|
# paths are owned by another ingress) to the canonical origin. Exact/longer
|
||||||
|
# paths on the same host in other ingresses still take precedence over "/".
|
||||||
|
# A snippet is used because the controller's annotation validation rejects
|
||||||
|
# "$request_uri" in permanent-redirect.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: www-redirect
|
||||||
|
namespace: {{ www_namespace }}
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: {{ www_cluster_issuer | to_json }}
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
nginx.ingress.kubernetes.io/enable-access-log: "false"
|
||||||
|
nginx.ingress.kubernetes.io/configuration-snippet: {{ ("return 301 https://" ~ www_hostname ~ "$request_uri;") | to_json }}
|
||||||
|
spec:
|
||||||
|
ingressClassName: {{ www_ingress_class }}
|
||||||
|
rules:
|
||||||
|
{% for host in www_redirect_hostnames %}
|
||||||
|
- host: {{ host }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
path: /
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: httpd
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
{% endfor %}
|
||||||
|
tls:
|
||||||
|
- hosts: {{ www_redirect_hostnames | to_json }}
|
||||||
|
secretName: www-redirect
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -4,8 +4,21 @@ www_hostname: www.eom.dev
|
|||||||
www_membership_image: registry.example.org/www-membership:2026-10-23
|
www_membership_image: registry.example.org/www-membership:2026-10-23
|
||||||
www_repo_version: refs/heads/main
|
www_repo_version: refs/heads/main
|
||||||
www_membership_secret_name: www-membership-credentials
|
www_membership_secret_name: www-membership-credentials
|
||||||
|
# Examples only: create dedicated groupOfUniqueNames entries and ACLs first.
|
||||||
|
# No automatic inheritance: include each group at every permitted level.
|
||||||
|
www_membership_service_groups:
|
||||||
|
"0": ["cn=community-chat,ou=membership-services,dc=eom,dc=dev"]
|
||||||
|
"1": ["cn=community-chat,ou=membership-services,dc=eom,dc=dev"]
|
||||||
|
"2": ["cn=community-chat,ou=membership-services,dc=eom,dc=dev", "cn=game-servers,ou=membership-services,dc=eom,dc=dev"]
|
||||||
|
"3": ["cn=community-chat,ou=membership-services,dc=eom,dc=dev", "cn=game-servers,ou=membership-services,dc=eom,dc=dev"]
|
||||||
|
"4": ["cn=community-chat,ou=membership-services,dc=eom,dc=dev", "cn=game-servers,ou=membership-services,dc=eom,dc=dev"]
|
||||||
|
"5": ["cn=community-chat,ou=membership-services,dc=eom,dc=dev", "cn=game-servers,ou=membership-services,dc=eom,dc=dev"]
|
||||||
www_membership_shkeeper_enabled: true
|
www_membership_shkeeper_enabled: true
|
||||||
www_membership_environment_overrides:
|
www_membership_environment_overrides:
|
||||||
|
# Optional plaintext LDAP for a trusted cluster only; sends passwords unencrypted.
|
||||||
|
# Enable BOTH lines together. Omit them to retain the role's verified TLS default.
|
||||||
|
# LDAP_URL: ldap://openldap.openldap.svc.cluster.local:389
|
||||||
|
# LDAP_ALLOW_INSECURE: "1"
|
||||||
LDAP_BIND_DN: cn=www-membership,ou=system,dc=eom,dc=dev
|
LDAP_BIND_DN: cn=www-membership,ou=system,dc=eom,dc=dev
|
||||||
LDAP_PASSWORD_SCHEME: "{ARGON2}"
|
LDAP_PASSWORD_SCHEME: "{ARGON2}"
|
||||||
EMAIL_HOST: smtp.eom.dev
|
EMAIL_HOST: smtp.eom.dev
|
||||||
|
|||||||
+13
-1
@@ -11,6 +11,10 @@
|
|||||||
- name: Supply nonsecret fixture settings
|
- name: Supply nonsecret fixture settings
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
www_membership_image: registry.example.org/www-membership:release-test
|
www_membership_image: registry.example.org/www-membership:release-test
|
||||||
|
www_redirect_hostnames: [apex.example.com]
|
||||||
|
www_membership_service_groups:
|
||||||
|
"0": ["cn=free-service,ou=groups,dc=example,dc=org"]
|
||||||
|
"2": ["cn=paid-service,ou=groups,dc=example,dc=org", "cn=second-service,ou=groups,dc=example,dc=org"]
|
||||||
_www_membership_revision: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
_www_membership_revision: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
www_membership_environment: "{{ www_membership_environment | combine({'MEMBERSHIP_PUBLIC_URL': 'https://' ~ www_hostname, 'LDAP_BIND_DN': 'cn=membership,dc=example,dc=org', 'LDAP_PASSWORD_SCHEME': '{ARGON2}', 'EMAIL_HOST': 'smtp.example.org', 'STRIPE_PORTAL_CONFIGURATION': 'bpc_test'}) }}"
|
www_membership_environment: "{{ www_membership_environment | combine({'MEMBERSHIP_PUBLIC_URL': 'https://' ~ www_hostname, 'LDAP_BIND_DN': 'cn=membership,dc=example,dc=org', 'LDAP_PASSWORD_SCHEME': '{ARGON2}', 'EMAIL_HOST': 'smtp.example.org', 'STRIPE_PORTAL_CONFIGURATION': 'bpc_test'}) }}"
|
||||||
- name: Render Apache proxy fixture
|
- name: Render Apache proxy fixture
|
||||||
@@ -27,12 +31,20 @@
|
|||||||
dest: "/tmp/www-test-{{ item }}.yml"
|
dest: "/tmp/www-test-{{ item }}.yml"
|
||||||
mode: "0600"
|
mode: "0600"
|
||||||
loop: [website, membership-config, membership-postgres, membership-migrate, membership-workloads]
|
loop: [website, membership-config, membership-postgres, membership-migrate, membership-workloads]
|
||||||
|
- name: Enable explicit plaintext LDAP fixture
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
www_membership_environment: "{{ www_membership_environment | combine({'LDAP_URL': 'ldap://openldap.openldap.svc.cluster.local:389', 'LDAP_ALLOW_INSECURE': '1'}) }}"
|
||||||
|
- name: Render plaintext LDAP configuration
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: ../templates/membership-config.yml.j2
|
||||||
|
dest: /tmp/www-test-plaintext-membership-config.yml
|
||||||
|
mode: "0600"
|
||||||
- name: Enable optional external database and private CA fixture
|
- name: Enable optional external database and private CA fixture
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
www_membership_postgres_managed: false
|
www_membership_postgres_managed: false
|
||||||
www_membership_ca_secret_name: private-ca
|
www_membership_ca_secret_name: private-ca
|
||||||
www_image_pull_secrets: [{name: registry-pull}]
|
www_image_pull_secrets: [{name: registry-pull}]
|
||||||
www_membership_environment: "{{ www_membership_environment | combine({'POSTGRES_HOST': 'postgres.example.org', 'POSTGRES_SSLMODE': 'verify-full', 'PGSSLROOTCERT': '/etc/membership/ca/db.pem', 'LDAP_CA_CERT_FILE': '/etc/membership/ca/ldap.pem', 'SHKEEPER_URL': 'https://crypto.example.org', 'SHKEEPER_CURRENCIES': 'BTC,XMR', 'SHKEEPER_CA_CERT_FILE': '/etc/membership/ca/shkeeper.pem'}) }}"
|
www_membership_environment: "{{ www_membership_environment | combine({'LDAP_URL': 'ldaps://ldap.example.com', 'LDAP_ALLOW_INSECURE': '0', 'POSTGRES_HOST': 'postgres.example.org', 'POSTGRES_SSLMODE': 'verify-full', 'PGSSLROOTCERT': '/etc/membership/ca/db.pem', 'LDAP_CA_CERT_FILE': '/etc/membership/ca/ldap.pem', 'SHKEEPER_URL': 'https://crypto.example.org', 'SHKEEPER_CURRENCIES': 'BTC,XMR', 'SHKEEPER_CA_CERT_FILE': '/etc/membership/ca/shkeeper.pem'}) }}"
|
||||||
- name: Render external provider variants
|
- name: Render external provider variants
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: "../templates/{{ item }}.yml.j2"
|
src: "../templates/{{ item }}.yml.j2"
|
||||||
|
|||||||
@@ -43,9 +43,20 @@ assert len({(obj["kind"], obj["metadata"]["name"]) for obj in all_objects}) == l
|
|||||||
config = find(all_objects, "ConfigMap", "membership-config")["data"]
|
config = find(all_objects, "ConfigMap", "membership-config")["data"]
|
||||||
assert all(isinstance(value, str) for value in config.values())
|
assert all(isinstance(value, str) for value in config.values())
|
||||||
assert config["MEMBERSHIP_PUBLIC_URL"] == "https://www.eom.dev"
|
assert config["MEMBERSHIP_PUBLIC_URL"] == "https://www.eom.dev"
|
||||||
|
assert config["LDAP_ALLOW_INSECURE"] == "0"
|
||||||
|
assert config["LDAP_URL"].startswith("ldaps://")
|
||||||
|
plaintext_config = find(docs("plaintext-membership-config"), "ConfigMap", "membership-config")["data"]
|
||||||
|
assert plaintext_config["LDAP_ALLOW_INSECURE"] == "1"
|
||||||
|
assert plaintext_config["LDAP_URL"] == "ldap://openldap.openldap.svc.cluster.local:389"
|
||||||
|
assert all(isinstance(value, str) for value in plaintext_config.values())
|
||||||
assert "MEMBERSHIP_SECRET_KEY" not in config and "SHKEEPER_API_KEY" not in config
|
assert "MEMBERSHIP_SECRET_KEY" not in config and "SHKEEPER_API_KEY" not in config
|
||||||
tiers = json.loads(find(all_objects, "ConfigMap", "membership-tiers")["data"]["tiers.json"])
|
tiers = json.loads(find(all_objects, "ConfigMap", "membership-tiers")["data"]["tiers.json"])
|
||||||
assert len(tiers) == 5 and tiers["level-4"]["usd_amount"] == "64.00"
|
service_groups = json.loads(find(all_objects, "ConfigMap", "membership-tiers")["data"]["service-groups.json"])
|
||||||
|
assert config["MEMBERSHIP_SERVICE_GROUPS_FILE"] == "/etc/membership/tiers/service-groups.json"
|
||||||
|
assert service_groups["0"] == ["cn=free-service,ou=groups,dc=example,dc=org"]
|
||||||
|
assert len(service_groups["2"]) == 2
|
||||||
|
assert len(tiers) == 5 and tiers["level-5"]["usd_amount"] == "64.00"
|
||||||
|
assert [tiers[f"level-{level}"]["level"] for level in range(1, 6)] == list(range(1, 6))
|
||||||
for name in ["membership", "membership-worker"]:
|
for name in ["membership", "membership-worker"]:
|
||||||
deployment = find(all_objects, "Deployment", name)
|
deployment = find(all_objects, "Deployment", name)
|
||||||
pod_checks(deployment["spec"]["template"]["spec"])
|
pod_checks(deployment["spec"]["template"]["spec"])
|
||||||
@@ -69,6 +80,9 @@ apache = find(all_objects, "ConfigMap", "httpd")["data"]
|
|||||||
assert "http://membership:8000/members/" in apache["membership.conf"]
|
assert "http://membership:8000/members/" in apache["membership.conf"]
|
||||||
assert "X-Forwarded-Proto" in apache["membership.conf"]
|
assert "X-Forwarded-Proto" in apache["membership.conf"]
|
||||||
assert "Require all denied" in apache["membership.conf"]
|
assert "Require all denied" in apache["membership.conf"]
|
||||||
|
assert "LimitRequestBody 262144" in apache["membership.conf"]
|
||||||
|
assert '<Location "/members/profile/">' in apache["membership.conf"]
|
||||||
|
assert "LimitRequestBody 2097152" in apache["membership.conf"]
|
||||||
assert "env=!membership_sensitive" in apache["httpd.conf"]
|
assert "env=!membership_sensitive" in apache["httpd.conf"]
|
||||||
assert "LoadModule proxy_http_module" in apache["httpd.conf"]
|
assert "LoadModule proxy_http_module" in apache["httpd.conf"]
|
||||||
source = find(all_objects, "Deployment", "httpd")["spec"]["template"]["spec"]["initContainers"][0]
|
source = find(all_objects, "Deployment", "httpd")["spec"]["template"]["spec"]["initContainers"][0]
|
||||||
@@ -76,14 +90,19 @@ assert "rm -rf" not in source["command"][-1]
|
|||||||
assert 'cp -a /tmp/www/htdocs/. /data/' in source["command"][-1]
|
assert 'cp -a /tmp/www/htdocs/. /data/' in source["command"][-1]
|
||||||
ingress = find(all_objects, "Ingress", "www")
|
ingress = find(all_objects, "Ingress", "www")
|
||||||
assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/enable-access-log"] == "false"
|
assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/enable-access-log"] == "false"
|
||||||
|
assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/proxy-body-size"] == "2m"
|
||||||
assert ingress["spec"]["rules"][0]["http"]["paths"][0]["backend"]["service"]["name"] == "httpd"
|
assert ingress["spec"]["rules"][0]["http"]["paths"][0]["backend"]["service"]["name"] == "httpd"
|
||||||
|
redirect = find(all_objects, "Ingress", "www-redirect")
|
||||||
|
assert redirect["spec"]["rules"][0]["host"] == "apex.example.com"
|
||||||
|
assert redirect["metadata"]["annotations"]["nginx.ingress.kubernetes.io/configuration-snippet"] == "return 301 " + config["MEMBERSHIP_PUBLIC_URL"] + "$request_uri;"
|
||||||
for obj in docs("external-membership-workloads"):
|
for obj in docs("external-membership-workloads"):
|
||||||
if obj["kind"] == "Deployment":
|
if obj["kind"] == "Deployment":
|
||||||
pod_checks(obj["spec"]["template"]["spec"], external=True)
|
pod_checks(obj["spec"]["template"]["spec"], external=True)
|
||||||
external_config = docs("external-membership-config")
|
external_config = docs("external-membership-config")
|
||||||
assert not any(obj["kind"] == "NetworkPolicy" and obj["metadata"]["name"] == "membership-db" for obj in external_config)
|
assert not any(obj["kind"] == "NetworkPolicy" and obj["metadata"]["name"] == "membership-db" for obj in external_config)
|
||||||
assert find(external_config, "ConfigMap", "membership-config")["data"]["POSTGRES_SSLMODE"] == "verify-full"
|
assert find(external_config, "ConfigMap", "membership-config")["data"]["POSTGRES_SSLMODE"] == "verify-full"
|
||||||
|
assert find(external_config, "ConfigMap", "membership-config")["data"]["LDAP_ALLOW_INSECURE"] == "0"
|
||||||
disabled = docs("disabled-website")
|
disabled = docs("disabled-website")
|
||||||
assert not any(obj["kind"] == "OnionService" for obj in disabled)
|
assert not any(obj["kind"] == "OnionService" for obj in disabled)
|
||||||
assert "ProxyPass" not in find(disabled, "ConfigMap", "httpd")["data"]["membership.conf"]
|
assert "ProxyPass" not in find(disabled, "ConfigMap", "httpd")["data"]["membership.conf"]
|
||||||
print(f"PASS: {len(all_objects)} Kubernetes objects; default, external/CA/SHKeeper and disabled variants")
|
print(f"PASS: {len(all_objects)} Kubernetes objects; default, plaintext LDAP, external/CA/SHKeeper and disabled variants")
|
||||||
|
|||||||
Reference in New Issue
Block a user