mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-09-10 12:27:15 +02:00
Compare commits
5 Commits
e032fd1aa4
...
56b3f854c5
Author | SHA1 | Date | |
---|---|---|---|
56b3f854c5 | |||
a93e1520d4 | |||
1486862327 | |||
3600874223 | |||
a3fd74c2e0 |
9
Makefile
9
Makefile
@@ -1,6 +1,6 @@
|
|||||||
ROLES_DIR := ./roles
|
ROLES_DIR := ./roles
|
||||||
APPLICATIONS_OUT := ./group_vars/all/04_applications.yml
|
APPLICATIONS_OUT := ./group_vars/all/04_applications.yml
|
||||||
APPLICATIONS_SCRIPT := ./cli/generate-applications-defaults.py
|
APPLICATIONS_SCRIPT := ./cli/generate_applications.py
|
||||||
USERS_OUT := ./group_vars/all/03_users.yml
|
USERS_OUT := ./group_vars/all/03_users.yml
|
||||||
USERS_SCRIPT := ./cli/generate_users.py
|
USERS_SCRIPT := ./cli/generate_users.py
|
||||||
INCLUDES_OUT := ./tasks/utils/docker-roles.yml
|
INCLUDES_OUT := ./tasks/utils/docker-roles.yml
|
||||||
@@ -9,6 +9,7 @@ INCLUDES_SCRIPT := ./cli/generate_playbook.py
|
|||||||
EXTRA_USERS := $(shell \
|
EXTRA_USERS := $(shell \
|
||||||
find $(ROLES_DIR) -maxdepth 1 -type d -name 'docker*' -printf '%f\n' \
|
find $(ROLES_DIR) -maxdepth 1 -type d -name 'docker*' -printf '%f\n' \
|
||||||
| sed -E 's/^docker[_-]?//' \
|
| sed -E 's/^docker[_-]?//' \
|
||||||
|
| grep -E -x '[a-z0-9]+' \
|
||||||
| paste -sd, - \
|
| paste -sd, - \
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +31,5 @@ install: build
|
|||||||
@echo "⚙️ Install complete."
|
@echo "⚙️ Install complete."
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@echo "🧪 Running Unit Tests..."
|
@echo "🧪 Running Tests..."
|
||||||
python -m unittest discover -s tests/unit
|
python -m unittest discover -s tests
|
||||||
@echo "🔬 Running Integration Tests..."
|
|
||||||
python -m unittest discover -s tests/integration
|
|
0
__init__.py
Normal file
0
__init__.py
Normal file
@@ -6,6 +6,11 @@ import yaml
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
plugin_path = Path(__file__).resolve().parent / ".." / "lookup_plugins"
|
||||||
|
sys.path.insert(0, str(plugin_path))
|
||||||
|
|
||||||
|
from application_gid import LookupModule
|
||||||
|
|
||||||
def load_yaml_file(path):
|
def load_yaml_file(path):
|
||||||
"""Load a YAML file if it exists, otherwise return an empty dict."""
|
"""Load a YAML file if it exists, otherwise return an empty dict."""
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
@@ -37,6 +42,7 @@ def main():
|
|||||||
# Initialize result structure
|
# Initialize result structure
|
||||||
result = {"defaults_applications": {}}
|
result = {"defaults_applications": {}}
|
||||||
|
|
||||||
|
gid_lookup = LookupModule()
|
||||||
# Process each role for application configs
|
# Process each role for application configs
|
||||||
for role_dir in sorted(roles_dir.iterdir()):
|
for role_dir in sorted(roles_dir.iterdir()):
|
||||||
role_name = role_dir.name
|
role_name = role_dir.name
|
||||||
@@ -67,6 +73,12 @@ def main():
|
|||||||
|
|
||||||
config_data = load_yaml_file(config_file)
|
config_data = load_yaml_file(config_file)
|
||||||
if config_data:
|
if config_data:
|
||||||
|
try:
|
||||||
|
gid_number = gid_lookup.run([application_id], roles_dir=str(roles_dir))[0]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: failed to determine gid for '{application_id}': {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
config_data["group_id"] = gid_number
|
||||||
result["defaults_applications"][application_id] = config_data
|
result["defaults_applications"][application_id] = config_data
|
||||||
users_meta_file = role_dir / "meta" / "users.yml"
|
users_meta_file = role_dir / "meta" / "users.yml"
|
||||||
transformed_users = {}
|
transformed_users = {}
|
@@ -50,6 +50,7 @@ def build_users(defs, primary_domain, start_id, become_pwd):
|
|||||||
username = overrides.get('username', key)
|
username = overrides.get('username', key)
|
||||||
email = overrides.get('email', f"{username}@{primary_domain}")
|
email = overrides.get('email', f"{username}@{primary_domain}")
|
||||||
description = overrides.get('description')
|
description = overrides.get('description')
|
||||||
|
roles = overrides.get('roles',[])
|
||||||
password = overrides.get('password',become_pwd)
|
password = overrides.get('password',become_pwd)
|
||||||
# UID assignment
|
# UID assignment
|
||||||
if 'uid' in overrides:
|
if 'uid' in overrides:
|
||||||
@@ -63,12 +64,11 @@ def build_users(defs, primary_domain, start_id, become_pwd):
|
|||||||
'email': email,
|
'email': email,
|
||||||
'password': password,
|
'password': password,
|
||||||
'uid': uid,
|
'uid': uid,
|
||||||
'gid': gid
|
'gid': gid,
|
||||||
|
'roles': roles
|
||||||
}
|
}
|
||||||
if description is not None:
|
if description is not None:
|
||||||
entry['description'] = description
|
entry['description'] = description
|
||||||
if overrides.get('is_admin', False):
|
|
||||||
entry['is_admin'] = True
|
|
||||||
|
|
||||||
users[key] = entry
|
users[key] = entry
|
||||||
|
|
||||||
|
@@ -35,16 +35,17 @@ ldap:
|
|||||||
# Typically: “cn=admin,cn=config”
|
# Typically: “cn=admin,cn=config”
|
||||||
configuration: "cn={{ applications.ldap.users.administrator.username }},cn=config"
|
configuration: "cn={{ applications.ldap.users.administrator.username }},cn=config"
|
||||||
|
|
||||||
|
ou:
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Organizational Units (OUs)
|
# Organizational Units (OUs)
|
||||||
# Pre-created containers in the data tree to organize entries.
|
# Pre-created containers in the directory tree to logically separate entries:
|
||||||
# – users: Where all person/posixAccount entries live.
|
# – users: Contains all user objects (person/posixAccount entries).
|
||||||
# – groups: Where you define your application or business groups.
|
# – groups: Contains organizational or business groups (e.g., departments, teams).
|
||||||
# – roles: A flat container for application-role entries (e.g. “cn=app1-user”).
|
# – roles: Contains application-specific RBAC roles
|
||||||
users: "ou=users,{{ _ldap_dn_base }}"
|
# (e.g., "cn=app1-user", "cn=yourls-admin").
|
||||||
groups: "ou=groups,{{ _ldap_dn_base }}"
|
users: "ou=users,{{ _ldap_dn_base }}"
|
||||||
application_roles: "ou=application_roles,{{ _ldap_dn_base }}"
|
groups: "ou=groups,{{ _ldap_dn_base }}"
|
||||||
|
roles: "ou=roles,{{ _ldap_dn_base }}"
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Additional Notes
|
# Additional Notes
|
||||||
|
42
lookup_plugins/application_gid.py
Normal file
42
lookup_plugins/application_gid.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from ansible.plugins.lookup import LookupBase
|
||||||
|
from ansible.errors import AnsibleError
|
||||||
|
|
||||||
|
class LookupModule(LookupBase):
|
||||||
|
|
||||||
|
def run(self, terms, variables=None, **kwargs):
|
||||||
|
application_id = terms[0]
|
||||||
|
base_gid = kwargs.get('base_gid', 10000)
|
||||||
|
roles_dir = kwargs.get('roles_dir', 'roles')
|
||||||
|
|
||||||
|
if not os.path.isdir(roles_dir):
|
||||||
|
raise AnsibleError(f"Roles directory '{roles_dir}' not found")
|
||||||
|
|
||||||
|
matched_roles = []
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(roles_dir):
|
||||||
|
if os.path.basename(root) == "vars" and "main.yml" in files:
|
||||||
|
vars_path = os.path.join(root, "main.yml")
|
||||||
|
try:
|
||||||
|
with open(vars_path, 'r') as f:
|
||||||
|
data = yaml.safe_load(f) or {}
|
||||||
|
app_id = data.get('application_id')
|
||||||
|
if app_id:
|
||||||
|
matched_roles.append((app_id, vars_path))
|
||||||
|
except Exception as e:
|
||||||
|
raise AnsibleError(f"Error parsing {vars_path}: {e}")
|
||||||
|
|
||||||
|
# sort alphabetically by application_id
|
||||||
|
sorted_ids = sorted(app_id for app_id, _ in matched_roles)
|
||||||
|
|
||||||
|
try:
|
||||||
|
index = sorted_ids.index(application_id)
|
||||||
|
except ValueError:
|
||||||
|
raise AnsibleError(f"Application ID '{application_id}' not found in any role")
|
||||||
|
|
||||||
|
return [base_gid + index]
|
@@ -166,7 +166,7 @@ run:
|
|||||||
|
|
||||||
# LDAP additional configuration
|
# LDAP additional configuration
|
||||||
- exec: rails r "SiteSetting.ldap_user_filter = '{{ ldap.filters.users.login }}'"
|
- exec: rails r "SiteSetting.ldap_user_filter = '{{ ldap.filters.users.login }}'"
|
||||||
- exec: rails r "SiteSetting.ldap_group_base_dn = '{{ ldap.dn.groups }}'"
|
- exec: rails r "SiteSetting.ldap_group_base_dn = '{{ ldap.dn.ou.groups }}'"
|
||||||
- exec: rails r "SiteSetting.ldap_group_member_check = 'memberUid'"
|
- exec: rails r "SiteSetting.ldap_group_member_check = 'memberUid'"
|
||||||
|
|
||||||
- exec: rails r "SiteSetting.ldap_sync_period = 1"
|
- exec: rails r "SiteSetting.ldap_sync_period = 1"
|
||||||
|
@@ -69,7 +69,7 @@ ESPOCRM_CONFIG_LDAP_PORT={{ ldap.server.port }}
|
|||||||
ESPOCRM_CONFIG_LDAP_SECURITY={{ ldap.server.security }}
|
ESPOCRM_CONFIG_LDAP_SECURITY={{ ldap.server.security }}
|
||||||
ESPOCRM_CONFIG_LDAP_USERNAME={{ ldap.dn.administrator.data }}
|
ESPOCRM_CONFIG_LDAP_USERNAME={{ ldap.dn.administrator.data }}
|
||||||
ESPOCRM_CONFIG_LDAP_PASSWORD={{ ldap.bind_credential }}
|
ESPOCRM_CONFIG_LDAP_PASSWORD={{ ldap.bind_credential }}
|
||||||
ESPOCRM_CONFIG_LDAP_BASE_DN={{ ldap.dn.users }}
|
ESPOCRM_CONFIG_LDAP_BASE_DN={{ ldap.dn.ou.users }}
|
||||||
ESPOCRM_CONFIG_LDAP_USER_LOGIN_FILTER=(sAMAccountName=%USERNAME%)
|
ESPOCRM_CONFIG_LDAP_USER_LOGIN_FILTER=(sAMAccountName=%USERNAME%)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ gitea_ldap_auth_args:
|
|||||||
- '--security-protocol "{{ ldap.server.security | trim or "unencrypted" }}"'
|
- '--security-protocol "{{ ldap.server.security | trim or "unencrypted" }}"'
|
||||||
- '--bind-dn "{{ ldap.dn.administrator.data }}"'
|
- '--bind-dn "{{ ldap.dn.administrator.data }}"'
|
||||||
- '--bind-password "{{ ldap.bind_credential }}"'
|
- '--bind-password "{{ ldap.bind_credential }}"'
|
||||||
- '--user-search-base "{{ ldap.dn.users }}"'
|
- '--user-search-base "{{ ldap.dn.ou.users }}"'
|
||||||
- '--user-filter "(&(objectClass=inetOrgPerson)(uid=%s))"'
|
- '--user-filter "(&(objectClass=inetOrgPerson)(uid=%s))"'
|
||||||
- '--username-attribute "{{ ldap.attributes.user_id }}"'
|
- '--username-attribute "{{ ldap.attributes.user_id }}"'
|
||||||
- '--firstname-attribute "{{ ldap.attributes.firstname }}"'
|
- '--firstname-attribute "{{ ldap.attributes.firstname }}"'
|
||||||
|
@@ -2045,7 +2045,7 @@
|
|||||||
"false"
|
"false"
|
||||||
],
|
],
|
||||||
"usersDn": [
|
"usersDn": [
|
||||||
"{{ldap.dn.users}}"
|
"{{ldap.dn.ou.users}}"
|
||||||
],
|
],
|
||||||
"connectionPooling": [
|
"connectionPooling": [
|
||||||
"true"
|
"true"
|
||||||
|
@@ -46,6 +46,10 @@ docker exec -it ldap bash -c "ldapsearch -LLL -o ldif-wrap=no -x -D \"\$LDAP_ADM
|
|||||||
### Delete Groups and Subgroup
|
### Delete Groups and Subgroup
|
||||||
To delete the group inclusive all subgroups use:
|
To delete the group inclusive all subgroups use:
|
||||||
```bash
|
```bash
|
||||||
docker exec -it ldap bash -c "ldapsearch -LLL -o ldif-wrap=no -x -D \"\$LDAP_ADMIN_DN\" -w \"\$LDAP_ADMIN_PASSWORD\" -b \"ou=applications,ou=groups,\$LDAP_ROOT\" dn | sed -n 's/^dn: //p' | tac | while read -r dn; do echo \"Deleting \$dn\"; ldapdelete -x -D \"\$LDAP_ADMIN_DN\" -w \"\$LDAP_ADMIN_PASSWORD\" \"\$dn\"; done"
|
docker exec -it ldap \
|
||||||
|
ldapdelete -x \
|
||||||
|
-D "$LDAP_ADMIN_DN" \
|
||||||
|
-w "$LDAP_ADMIN_PASSWORD" \
|
||||||
|
-r \
|
||||||
|
"ou=groups,dc=veen,dc=world"
|
||||||
```
|
```
|
@@ -3,7 +3,7 @@
|
|||||||
server_uri: "{{ ldap_server_uri }}"
|
server_uri: "{{ ldap_server_uri }}"
|
||||||
bind_dn: "{{ ldap.dn.administrator.data }}"
|
bind_dn: "{{ ldap.dn.administrator.data }}"
|
||||||
bind_pw: "{{ ldap.bind_credential }}"
|
bind_pw: "{{ ldap.bind_credential }}"
|
||||||
dn: "{{ ldap.dn.users }}"
|
dn: "{{ ldap.dn.ou.users }}"
|
||||||
scope: subordinate
|
scope: subordinate
|
||||||
filter: "{{ ldap.filters.users.all }}"
|
filter: "{{ ldap.filters.users.all }}"
|
||||||
attrs:
|
attrs:
|
||||||
|
@@ -63,7 +63,7 @@
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
- name: Ensure LDAP users exist
|
- name: Ensure LDAP users exist
|
||||||
community.general.ldap_entry:
|
community.general.ldap_entry:
|
||||||
dn: "{{ ldap.attributes.user_id }}={{ item.key }},{{ ldap.dn.users }}"
|
dn: "{{ ldap.attributes.user_id }}={{ item.key }},{{ ldap.dn.ou.users }}"
|
||||||
server_uri: "{{ ldap_server_uri }}"
|
server_uri: "{{ ldap_server_uri }}"
|
||||||
bind_dn: "{{ ldap.dn.administrator.data }}"
|
bind_dn: "{{ ldap.dn.administrator.data }}"
|
||||||
bind_pw: "{{ ldap.bind_credential }}"
|
bind_pw: "{{ ldap.bind_credential }}"
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
- name: Ensure required objectClass values and mail address are present
|
- name: Ensure required objectClass values and mail address are present
|
||||||
community.general.ldap_attrs:
|
community.general.ldap_attrs:
|
||||||
dn: "{{ ldap.attributes.user_id }}={{ item.key }},{{ ldap.dn.users }}"
|
dn: "{{ ldap.attributes.user_id }}={{ item.key }},{{ ldap.dn.ou.users }}"
|
||||||
server_uri: "{{ ldap_server_uri }}"
|
server_uri: "{{ ldap_server_uri }}"
|
||||||
bind_dn: "{{ ldap.dn.administrator.data }}"
|
bind_dn: "{{ ldap.dn.administrator.data }}"
|
||||||
bind_pw: "{{ ldap.bind_credential }}"
|
bind_pw: "{{ ldap.bind_credential }}"
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
|
|
||||||
- name: "Ensure container for application roles exists"
|
- name: "Ensure container for application roles exists"
|
||||||
community.general.ldap_entry:
|
community.general.ldap_entry:
|
||||||
dn: "{{ ldap.dn.application_roles }}"
|
dn: "{{ ldap.dn.ou.roles }}"
|
||||||
server_uri: "{{ ldap_server_uri }}"
|
server_uri: "{{ ldap_server_uri }}"
|
||||||
bind_dn: "{{ ldap.dn.administrator.data }}"
|
bind_dn: "{{ ldap.dn.administrator.data }}"
|
||||||
bind_pw: "{{ ldap.bind_credential }}"
|
bind_pw: "{{ ldap.bind_credential }}"
|
||||||
|
@@ -1,42 +1,30 @@
|
|||||||
{% for app, config in applications.items() %}
|
{%- for application_id, application_config in applications.items() %}
|
||||||
dn: cn={{ app }}-administrator,{{ldap.dn.application_roles}}
|
{%- set base_roles = application_config.rbac.roles | default({}) %}
|
||||||
|
{%- set roles = base_roles | combine({
|
||||||
|
'administrator': {
|
||||||
|
'description': 'Has full administrative access: manage themes, plugins, settings, and users'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
%}
|
||||||
|
|
||||||
|
{%- for role_name, role_conf in roles.items() %}
|
||||||
|
dn: cn={{ application_id }}-{{ role_name }},{{ ldap.dn.ou.roles }}
|
||||||
objectClass: top
|
objectClass: top
|
||||||
objectClass: organizationalRole
|
objectClass: organizationalRole
|
||||||
cn: {{ app }}-administrator
|
objectClass: posixGroup
|
||||||
description: Administrator role for {{ app }} (automatically generated)
|
gidNumber: {{ application_config['group_id'] }}
|
||||||
|
cn: {{ application_id }}-{{ role_name }}
|
||||||
|
description: {{ role_conf.description }}
|
||||||
|
|
||||||
dn: cn={{ app }}-user,{{ldap.dn.application_roles}}
|
{%- for username, user_config in users.items() %}
|
||||||
objectClass: top
|
{%- set user_roles = user_config.roles | default([]) %}
|
||||||
objectClass: organizationalRole
|
{%- if role_name in user_roles %}
|
||||||
cn: {{ app }}-user
|
dn: cn={{ application_id }}-{{ role_name }},{{ ldap.dn.ou.roles }}
|
||||||
description: Standard user role for {{ app }} (automatically generated)
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% for username, user in users.items() %}
|
|
||||||
|
|
||||||
#######################################################################
|
|
||||||
# Assign {{ username }} to application user roles
|
|
||||||
#######################################################################
|
|
||||||
{% for app, config in applications.items() %}
|
|
||||||
|
|
||||||
# Assign {{ username }} to {{ app }}-users
|
|
||||||
|
|
||||||
dn: cn={{ app }}-user,{{ ldap.dn.application_roles }}
|
|
||||||
changetype: modify
|
changetype: modify
|
||||||
add: roleOccupant
|
add: roleOccupant
|
||||||
roleOccupant: {{ ldap.attributes.user_id }}={{ username }},{{ ldap.dn.users }}
|
roleOccupant: {{ ldap.attributes.user_id }}={{ username }},{{ ldap.dn.ou.users }}
|
||||||
|
|
||||||
{% if users.is_admin | default(false) | bool %}
|
{%- endif %}
|
||||||
|
{%- endfor %}
|
||||||
# Assign {{ username }} to {{ app }}-administrator
|
{%- endfor %}
|
||||||
dn: cn={{ app }}-administrator,{{ ldap.dn.application_roles }}
|
{%- endfor %}
|
||||||
changetype: modify
|
|
||||||
add: roleOccupant
|
|
||||||
roleOccupant: {{ ldap.attributes.user_id }}={{ users.administrator.username }},{{ ldap.dn.users }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
@@ -32,7 +32,13 @@
|
|||||||
mailu_domain: "{{ primary_domain }}"
|
mailu_domain: "{{ primary_domain }}"
|
||||||
mailu_api_base_url: "http://127.0.0.1:8080/api/v1"
|
mailu_api_base_url: "http://127.0.0.1:8080/api/v1"
|
||||||
mailu_global_api_token: "{{ applications.mailu.credentials.api_token }}"
|
mailu_global_api_token: "{{ applications.mailu.credentials.api_token }}"
|
||||||
mailu_action: "{{ item.value.is_admin | default(false) | ternary('admin','user') }}"
|
mailu_action: >-
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
'administrator' in (item.value.get('roles', []))
|
||||||
|
)
|
||||||
|
| ternary('admin','user')
|
||||||
|
}}
|
||||||
mailu_user_key: "{{ item.key }}"
|
mailu_user_key: "{{ item.key }}"
|
||||||
mailu_user_name: "{{ item.value.username }}"
|
mailu_user_name: "{{ item.value.username }}"
|
||||||
mailu_password: "{{ item.value.password }}"
|
mailu_password: "{{ item.value.password }}"
|
||||||
|
@@ -1,5 +1,24 @@
|
|||||||
# Routines to create the administrator account
|
# Routines to create the administrator account
|
||||||
# @see https://chatgpt.com/share/67b9b12c-064c-800f-9354-8e42e6459764
|
# @see https://chatgpt.com/share/67b9b12c-064c-800f-9354-8e42e6459764
|
||||||
|
|
||||||
|
- name: Check health status of {{ item }} container
|
||||||
|
shell: |
|
||||||
|
cid=$(docker compose ps -q {{ item }})
|
||||||
|
docker inspect \
|
||||||
|
--format '{{ "{{.State.Health.Status}}" }}' \
|
||||||
|
$cid
|
||||||
|
args:
|
||||||
|
chdir: "{{ docker_compose.directories.instance }}"
|
||||||
|
register: healthcheck
|
||||||
|
retries: 60
|
||||||
|
delay: 5
|
||||||
|
until: healthcheck.stdout == "healthy"
|
||||||
|
loop:
|
||||||
|
- web
|
||||||
|
- streaming
|
||||||
|
- sidekiq
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item }}"
|
||||||
|
|
||||||
- name: Remove line containing "- administrator" from config/settings.yml to allow creating administrator account
|
- name: Remove line containing "- administrator" from config/settings.yml to allow creating administrator account
|
||||||
command:
|
command:
|
||||||
|
@@ -42,7 +42,7 @@ plugin_configuration:
|
|||||||
-
|
-
|
||||||
appid: "user_ldap"
|
appid: "user_ldap"
|
||||||
configkey: "s01ldap_base_users"
|
configkey: "s01ldap_base_users"
|
||||||
configvalue: "{{ldap.dn.users}}"
|
configvalue: "{{ldap.dn.ou.users}}"
|
||||||
|
|
||||||
-
|
-
|
||||||
appid: "user_ldap"
|
appid: "user_ldap"
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
http_address = "0.0.0.0:4180"
|
http_address = "0.0.0.0:4180"
|
||||||
cookie_secret = "{{ applications[oauth2_proxy_application_id].credentials.oauth2_proxy_cookie_secret }}"
|
cookie_secret = "{{ applications[oauth2_proxy_application_id].credentials.oauth2_proxy_cookie_secret }}"
|
||||||
email_domains = "{{ primary_domain }}"
|
|
||||||
cookie_secure = "true" # True is necessary to force the cookie set via https
|
cookie_secure = "true" # True is necessary to force the cookie set via https
|
||||||
upstreams = "http://{{ applications[oauth2_proxy_application_id].oauth2_proxy.application }}:{{ applications[oauth2_proxy_application_id].oauth2_proxy.port }}"
|
upstreams = "http://{{ applications[oauth2_proxy_application_id].oauth2_proxy.application }}:{{ applications[oauth2_proxy_application_id].oauth2_proxy.port }}"
|
||||||
cookie_domains = ["{{ domains | get_domain(oauth2_proxy_application_id) }}", "{{ domains | get_domain('keycloak') }}"] # Required so cookie can be read on all subdomains.
|
cookie_domains = ["{{ domains | get_domain(oauth2_proxy_application_id) }}", "{{ domains | get_domain('keycloak') }}"] # Required so cookie can be read on all subdomains.
|
||||||
@@ -14,7 +13,11 @@ oidc_issuer_url = "{{ oidc.client.issuer_url }}"
|
|||||||
provider = "oidc"
|
provider = "oidc"
|
||||||
provider_display_name = "Keycloak"
|
provider_display_name = "Keycloak"
|
||||||
|
|
||||||
# role restrictions
|
{% if applications[oauth2_proxy_application_id].oauth2_proxy.allowed_groups is defined %}
|
||||||
#cookie_roles = "realm_access.roles"
|
{# role based restrictions #}
|
||||||
#allowed_groups = "{{ applications[application_id].allowed_roles }}" # This is not correct here. needs to be placed in applications @todo move there when implementing
|
scope = "openid email profile groups"
|
||||||
# @see https://chatgpt.com/share/67f42607-bf68-800f-b587-bd56fe9067b5
|
oidc_groups_claim = "realm_access.roles"
|
||||||
|
allowed_groups = {{ applications[oauth2_proxy_application_id].oauth2_proxy.allowed_groups | tojson }}
|
||||||
|
{% else %}
|
||||||
|
email_domains = "{{ primary_domain }}"
|
||||||
|
{% endif %}
|
@@ -4,7 +4,7 @@ openproject_ldap:
|
|||||||
port: "{{ ldap.server.port }}" # LDAP server port (typically 389 or 636)
|
port: "{{ ldap.server.port }}" # LDAP server port (typically 389 or 636)
|
||||||
account: "{{ ldap.dn.administrator.data }}" # Bind DN (used for authentication)
|
account: "{{ ldap.dn.administrator.data }}" # Bind DN (used for authentication)
|
||||||
account_password: "{{ ldap.bind_credential }}" # Bind password
|
account_password: "{{ ldap.bind_credential }}" # Bind password
|
||||||
base_dn: "{{ ldap.dn.users }}" # Base DN for user search
|
base_dn: "{{ ldap.dn.ou.users }}" # Base DN for user search
|
||||||
attr_login: "{{ ldap.attributes.user_id }}" # LDAP attribute used for login
|
attr_login: "{{ ldap.attributes.user_id }}" # LDAP attribute used for login
|
||||||
attr_firstname: "givenName" # LDAP attribute for first name
|
attr_firstname: "givenName" # LDAP attribute for first name
|
||||||
attr_lastname: "{{ ldap.attributes.surname }}" # LDAP attribute for last name
|
attr_lastname: "{{ ldap.attributes.surname }}" # LDAP attribute for last name
|
||||||
|
@@ -19,9 +19,9 @@ openproject_rails_settings:
|
|||||||
|
|
||||||
openproject_filters:
|
openproject_filters:
|
||||||
administrators: >-
|
administrators: >-
|
||||||
{{ '(memberOf=cn=openproject-admins,' ~ ldap.dn.application_roles ~ ')'
|
{{ '(memberOf=cn=openproject-admins,' ~ ldap.dn.ou.roles ~ ')'
|
||||||
if applications[application_id].ldap.filters.administrators else '' }}
|
if applications[application_id].ldap.filters.administrators else '' }}
|
||||||
|
|
||||||
users: >-
|
users: >-
|
||||||
{{ '(memberOf=cn=openproject-users,' ~ ldap.dn.application_roles ~ ')'
|
{{ '(memberOf=cn=openproject-users,' ~ ldap.dn.ou.roles ~ ')'
|
||||||
if applications[application_id].ldap.filters.users else '' }}
|
if applications[application_id].ldap.filters.users else '' }}
|
||||||
|
@@ -2,10 +2,19 @@
|
|||||||
include_vars: vars/oidc-settings.yml
|
include_vars: vars/oidc-settings.yml
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Check if OIDC plugin is already installed
|
||||||
|
command: >
|
||||||
|
docker exec {{ container_name }} test -d /data/plugins/data/peertube-plugin-auth-openid-connect
|
||||||
|
register: peertube_oidc_plugin_check
|
||||||
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
- name: "Install auth-openid-connect plugin for Peertube"
|
- name: "Install auth-openid-connect plugin for Peertube"
|
||||||
command: >
|
command: >
|
||||||
docker exec {{ container_name }} \
|
docker exec {{ container_name }} \
|
||||||
npm run plugin:install -- --npm-name {{oidc_plugin}}
|
npm run plugin:install -- --npm-name {{oidc_plugin}}
|
||||||
|
when: peertube_oidc_plugin_check.rc != 0
|
||||||
|
notify: docker compose up
|
||||||
|
|
||||||
- name: "Update the settings column of the auth-openid-connect plugin"
|
- name: "Update the settings column of the auth-openid-connect plugin"
|
||||||
community.postgresql.postgresql_query:
|
community.postgresql.postgresql_query:
|
||||||
@@ -20,3 +29,5 @@
|
|||||||
enabled = TRUE,
|
enabled = TRUE,
|
||||||
uninstalled = FALSE
|
uninstalled = FALSE
|
||||||
WHERE name = 'auth-openid-connect';
|
WHERE name = 'auth-openid-connect';
|
||||||
|
notify: docker compose up
|
||||||
|
when: peertube_oidc_plugin_check.rc != 0
|
||||||
|
@@ -1,3 +1,13 @@
|
|||||||
|
---
|
||||||
|
- name: "Wait until Postgres is listening on port {{ database_port }}"
|
||||||
|
wait_for:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: "{{ database_port }}"
|
||||||
|
delay: 5
|
||||||
|
timeout: 300
|
||||||
|
state: started
|
||||||
|
|
||||||
|
# 1) Create the database
|
||||||
- name: "Create database: {{ database_name }}"
|
- name: "Create database: {{ database_name }}"
|
||||||
postgresql_db:
|
postgresql_db:
|
||||||
name: "{{ database_name }}"
|
name: "{{ database_name }}"
|
||||||
@@ -5,8 +15,9 @@
|
|||||||
login_user: postgres
|
login_user: postgres
|
||||||
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
||||||
login_host: 127.0.0.1
|
login_host: 127.0.0.1
|
||||||
login_port: "{{database_port}}"
|
login_port: "{{ database_port }}"
|
||||||
|
|
||||||
|
# 2) Create the database user (with password)
|
||||||
- name: "Create database user: {{ database_username }}"
|
- name: "Create database user: {{ database_username }}"
|
||||||
postgresql_user:
|
postgresql_user:
|
||||||
name: "{{ database_username }}"
|
name: "{{ database_username }}"
|
||||||
@@ -16,68 +27,72 @@
|
|||||||
login_user: postgres
|
login_user: postgres
|
||||||
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
||||||
login_host: 127.0.0.1
|
login_host: 127.0.0.1
|
||||||
login_port: "{{database_port}}"
|
login_port: "{{ database_port }}"
|
||||||
|
|
||||||
- name: "Set privileges for database user: {{ database_username }}"
|
# 3) Enable LOGIN for the role (removes NOLOGIN)
|
||||||
|
- name: "Enable login for role {{ database_username }}"
|
||||||
|
postgresql_query:
|
||||||
|
db: postgres
|
||||||
|
login_user: postgres
|
||||||
|
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
||||||
|
login_host: 127.0.0.1
|
||||||
|
login_port: "{{ database_port }}"
|
||||||
|
query: |
|
||||||
|
ALTER ROLE "{{ database_username }}"
|
||||||
|
WITH LOGIN;
|
||||||
|
|
||||||
|
# 4) Grant ALL privileges on all tables in the public schema
|
||||||
|
- name: "Grant ALL privileges on tables in public schema to {{ database_username }}"
|
||||||
postgresql_privs:
|
postgresql_privs:
|
||||||
db: "{{ database_name }}"
|
db: "{{ database_name }}"
|
||||||
role: "{{ database_username }}"
|
role: "{{ database_username }}"
|
||||||
objs: ALL_IN_SCHEMA
|
objs: ALL_IN_SCHEMA
|
||||||
privs: ALL
|
privs: ALL
|
||||||
type: table
|
type: table
|
||||||
state: present
|
schema: public
|
||||||
|
state: present
|
||||||
login_user: postgres
|
login_user: postgres
|
||||||
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
||||||
login_host: 127.0.0.1
|
login_host: 127.0.0.1
|
||||||
login_port: "{{database_port}}"
|
login_port: "{{ database_port }}"
|
||||||
|
|
||||||
- name: Grant all privileges at the database level
|
# 5) Grant ALL privileges at the database level
|
||||||
|
- name: "Grant all privileges on database {{ database_name }} to {{ database_username }}"
|
||||||
postgresql_privs:
|
postgresql_privs:
|
||||||
db: "{{ database_name }}"
|
db: "{{ database_name }}"
|
||||||
role: "{{ database_username }}"
|
role: "{{ database_username }}"
|
||||||
|
type: database
|
||||||
privs: ALL
|
privs: ALL
|
||||||
type: database
|
|
||||||
state: present
|
state: present
|
||||||
login_user: postgres
|
login_user: postgres
|
||||||
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
||||||
login_host: 127.0.0.1
|
login_host: 127.0.0.1
|
||||||
login_port: "{{database_port}}"
|
login_port: "{{ database_port }}"
|
||||||
|
|
||||||
- name: Grant all privileges on all tables in the public schema
|
# 6) Grant USAGE/CREATE on schema and set default privileges
|
||||||
postgresql_privs:
|
- name: "Set comprehensive schema privileges for {{ database_username }}"
|
||||||
db: "{{ database_name }}"
|
|
||||||
role: "{{ database_username }}"
|
|
||||||
objs: ALL_IN_SCHEMA
|
|
||||||
privs: ALL
|
|
||||||
type: table
|
|
||||||
schema: public
|
|
||||||
state: present
|
|
||||||
login_user: postgres
|
|
||||||
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
|
||||||
login_host: 127.0.0.1
|
|
||||||
login_port: "{{database_port}}"
|
|
||||||
|
|
||||||
- name: Set comprehensive privileges for user on public schema
|
|
||||||
postgresql_query:
|
postgresql_query:
|
||||||
db: "{{ database_name }}"
|
db: "{{ database_name }}"
|
||||||
login_user: postgres
|
login_user: postgres
|
||||||
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
||||||
login_host: 127.0.0.1
|
login_host: 127.0.0.1
|
||||||
login_port: "{{database_port}}"
|
|
||||||
query: |
|
|
||||||
GRANT USAGE ON SCHEMA public TO {{ database_username }};
|
|
||||||
GRANT CREATE ON SCHEMA public TO {{ database_username }};
|
|
||||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO {{ database_username }};
|
|
||||||
|
|
||||||
- name: Ensure PostGIS-related extensions are installed
|
|
||||||
community.postgresql.postgresql_ext:
|
|
||||||
db: "{{ database_name }}"
|
|
||||||
ext: "{{ item }}"
|
|
||||||
state: present
|
|
||||||
login_user: postgres
|
|
||||||
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
|
||||||
login_host: 127.0.0.1
|
|
||||||
login_port: "{{ database_port }}"
|
login_port: "{{ database_port }}"
|
||||||
|
query: |
|
||||||
|
GRANT USAGE ON SCHEMA public TO "{{ database_username }}";
|
||||||
|
GRANT CREATE ON SCHEMA public TO "{{ database_username }}";
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
||||||
|
GRANT ALL PRIVILEGES ON TABLES TO "{{ database_username }}";
|
||||||
|
|
||||||
|
# 7) Ensure PostGIS and related extensions are installed (if enabled)
|
||||||
|
- name: "Ensure PostGIS-related extensions are installed"
|
||||||
|
community.postgresql.postgresql_ext:
|
||||||
|
db: "{{ database_name }}"
|
||||||
|
ext: "{{ item }}"
|
||||||
|
state: present
|
||||||
|
login_user: postgres
|
||||||
|
login_password: "{{ applications[application_id].credentials.postgres_password }}"
|
||||||
|
login_host: 127.0.0.1
|
||||||
|
login_port: "{{ database_port }}"
|
||||||
loop:
|
loop:
|
||||||
- postgis
|
- postgis
|
||||||
- pg_trgm
|
- pg_trgm
|
||||||
|
@@ -45,7 +45,7 @@
|
|||||||
$s->ldap_server = "{{ ldap.server.uri }}";
|
$s->ldap_server = "{{ ldap.server.uri }}";
|
||||||
$s->ldap_port = {{ ldap.server.port }};
|
$s->ldap_port = {{ ldap.server.port }};
|
||||||
$s->ldap_uname = "{{ ldap.dn.administrator.data }}";
|
$s->ldap_uname = "{{ ldap.dn.administrator.data }}";
|
||||||
$s->ldap_basedn = "{{ ldap.dn.users }}";
|
$s->ldap_basedn = "{{ ldap.dn.ou.users }}";
|
||||||
$s->ldap_filter = "&(objectClass=inetOrgPerson)";
|
$s->ldap_filter = "&(objectClass=inetOrgPerson)";
|
||||||
$s->ldap_username_field = "{{ ldap.attributes.user_id }}";
|
$s->ldap_username_field = "{{ ldap.attributes.user_id }}";
|
||||||
$s->ldap_fname_field = "{{ ldap.attributes.firstname }}";
|
$s->ldap_fname_field = "{{ ldap.attributes.firstname }}";
|
||||||
|
@@ -39,3 +39,15 @@ csp:
|
|||||||
domains:
|
domains:
|
||||||
canonical:
|
canonical:
|
||||||
- "blog.{{ primary_domain }}"
|
- "blog.{{ primary_domain }}"
|
||||||
|
rbac:
|
||||||
|
roles:
|
||||||
|
subscriber:
|
||||||
|
description: "Can read posts and leave comments but cannot write or manage content"
|
||||||
|
author:
|
||||||
|
description: "Can write and manage own posts"
|
||||||
|
contributor:
|
||||||
|
description: "Can write and submit posts for review but cannot publish"
|
||||||
|
editor:
|
||||||
|
description: "Can publish and manage all posts, including those by other users"
|
||||||
|
administrator:
|
||||||
|
description: "Has full administrative access: manage themes, plugins, settings, and users"
|
||||||
|
@@ -4,4 +4,6 @@ YOURLS_DB_PASS: "{{database_password}}"
|
|||||||
YOURLS_DB_NAME: "{{database_name}}"
|
YOURLS_DB_NAME: "{{database_name}}"
|
||||||
YOURLS_SITE: "{{ web_protocol }}://{{domains | get_domain(application_id)}}"
|
YOURLS_SITE: "{{ web_protocol }}://{{domains | get_domain(application_id)}}"
|
||||||
YOURLS_USER: "{{applications.yourls.users.administrator.username}}"
|
YOURLS_USER: "{{applications.yourls.users.administrator.username}}"
|
||||||
YOURLS_PASS: "{{applications[application_id].credentials.administrator_password}}"
|
YOURLS_PASS: "{{applications[application_id].credentials.administrator_password}}"
|
||||||
|
# The following deactivates the login mask for admins, if the oauth2 proxy is activated
|
||||||
|
YOURLS_PRIVATE: "{{not (applications | is_feature_enabled('oauth2', application_id))}}"
|
@@ -2,6 +2,8 @@ version: "latest"
|
|||||||
oauth2_proxy:
|
oauth2_proxy:
|
||||||
application: "application"
|
application: "application"
|
||||||
port: "80"
|
port: "80"
|
||||||
|
allowed_groups:
|
||||||
|
- "yourls-administrator"
|
||||||
acl:
|
acl:
|
||||||
blacklist:
|
blacklist:
|
||||||
- "/admin/" # Protects the admin area
|
- "/admin/" # Protects the admin area
|
||||||
|
@@ -6,4 +6,5 @@ users:
|
|||||||
password: "{{ ansible_become_password }}"
|
password: "{{ ansible_become_password }}"
|
||||||
uid: 1001
|
uid: 1001
|
||||||
gid: 1001
|
gid: 1001
|
||||||
is_admin: true
|
roles:
|
||||||
|
- administrator
|
||||||
|
@@ -44,9 +44,6 @@ users:
|
|||||||
proxy:
|
proxy:
|
||||||
username: proxy
|
username: proxy
|
||||||
description: "Proxy user"
|
description: "Proxy user"
|
||||||
www-data:
|
|
||||||
username: www-data
|
|
||||||
description: "Web server user"
|
|
||||||
backup:
|
backup:
|
||||||
username: backup
|
username: backup
|
||||||
description: "Backup operator"
|
description: "Backup operator"
|
||||||
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
62
tests/unit/test_application_gid.py
Normal file
62
tests/unit/test_application_gid.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import unittest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
dir_path = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), '../../lookup_plugins')
|
||||||
|
)
|
||||||
|
sys.path.insert(0, dir_path)
|
||||||
|
|
||||||
|
from application_gid import LookupModule
|
||||||
|
|
||||||
|
|
||||||
|
class TestApplicationGidLookup(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Create a temporary roles directory
|
||||||
|
self.temp_dir = tempfile.mkdtemp()
|
||||||
|
self.roles_dir = os.path.join(self.temp_dir, "roles")
|
||||||
|
os.mkdir(self.roles_dir)
|
||||||
|
|
||||||
|
# Define mock application_ids
|
||||||
|
self.applications = {
|
||||||
|
"nextcloud": "docker-nextcloud",
|
||||||
|
"moodle": "docker-moodle",
|
||||||
|
"wordpress": "docker-wordpress",
|
||||||
|
"taiga": "docker-taiga"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create fake role dirs and vars/main.yml
|
||||||
|
for app_id, dirname in self.applications.items():
|
||||||
|
role_path = os.path.join(self.roles_dir, dirname, "vars")
|
||||||
|
os.makedirs(role_path)
|
||||||
|
with open(os.path.join(role_path, "main.yml"), "w") as f:
|
||||||
|
yaml.dump({"application_id": app_id}, f)
|
||||||
|
|
||||||
|
self.lookup = LookupModule()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.temp_dir)
|
||||||
|
|
||||||
|
def test_gid_lookup(self):
|
||||||
|
# The sorted application_ids: [moodle, nextcloud, taiga, wordpress]
|
||||||
|
expected_order = ["moodle", "nextcloud", "taiga", "wordpress"]
|
||||||
|
for i, app_id in enumerate(expected_order):
|
||||||
|
result = self.lookup.run([app_id], roles_dir=self.roles_dir)
|
||||||
|
self.assertEqual(result, [10000 + i])
|
||||||
|
|
||||||
|
def test_custom_base_gid(self):
|
||||||
|
result = self.lookup.run(["taiga"], roles_dir=self.roles_dir, base_gid=20000)
|
||||||
|
self.assertEqual(result, [20002]) # 2nd index in sorted list
|
||||||
|
|
||||||
|
def test_application_id_not_found(self):
|
||||||
|
with self.assertRaises(Exception) as context:
|
||||||
|
self.lookup.run(["unknownapp"], roles_dir=self.roles_dir)
|
||||||
|
self.assertIn("Application ID 'unknownapp' not found", str(context.exception))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
@@ -43,7 +43,7 @@ class TestGenerateDefaultApplicationsUsers(unittest.TestCase):
|
|||||||
When a users.yml exists with defined users, the script should inject a 'users'
|
When a users.yml exists with defined users, the script should inject a 'users'
|
||||||
mapping in the generated YAML, mapping each username to a Jinja2 reference.
|
mapping in the generated YAML, mapping each username to a Jinja2 reference.
|
||||||
"""
|
"""
|
||||||
script_path = Path(__file__).resolve().parents[2] / "cli" / "generate-applications-defaults.py"
|
script_path = Path(__file__).resolve().parents[2] / "cli" / "generate_applications.py"
|
||||||
result = subprocess.run([
|
result = subprocess.run([
|
||||||
"python3", str(script_path),
|
"python3", str(script_path),
|
||||||
"--roles-dir", str(self.roles_dir),
|
"--roles-dir", str(self.roles_dir),
|
||||||
|
@@ -29,7 +29,7 @@ class TestGenerateDefaultApplications(unittest.TestCase):
|
|||||||
shutil.rmtree(self.temp_dir)
|
shutil.rmtree(self.temp_dir)
|
||||||
|
|
||||||
def test_script_generates_expected_yaml(self):
|
def test_script_generates_expected_yaml(self):
|
||||||
script_path = Path(__file__).resolve().parent.parent.parent / "cli" / "generate-applications-defaults.py"
|
script_path = Path(__file__).resolve().parent.parent.parent / "cli" / "generate_applications.py"
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[
|
[
|
||||||
|
Reference in New Issue
Block a user