Add phiadm

This commit is contained in:
sfigato 2022-02-03 21:31:29 +01:00
parent fa7b633a16
commit b376d1f270
Signed by: blallo
GPG Key ID: 0CBE577C9B72DC3F
10 changed files with 294 additions and 1 deletions

View File

@ -10,7 +10,9 @@ setup(
author_email="unit@paranoici.org",
package_dir={"": "src"},
packages=find_packages("src"),
entry_points={"console_scripts": ["phid=phi.web.app:cli"]},
entry_points={
"console_scripts": ["phid=phi.web.app:cli", "phiadm=phi.cli.adm:cli"]
},
setup_requires=["pytest-runner"],
install_requires=[
"aiohttp==3.8.1",

0
src/phi/cli/__init__.py Normal file
View File

119
src/phi/cli/adm.py Normal file
View File

@ -0,0 +1,119 @@
# -*- encoding: utf-8 -*-
import importlib.resources
from fnmatch import fnmatch
import re
import click
from phi.cli.utils import generate_from_templates
from phi.security import hash_pass
SERVICE_RE = re.compile(r"^(.+?):(.+)$")
templates = [
f.name.strip(".j2")
for f in importlib.resources.files("phi.cli.templates").iterdir()
if not f.name.endswith(".py")
]
def debug(ctx, out):
if ctx.obj["debug"]:
click.echo(f"DEBUG: {out}", err=True)
def validate_services(ctx, _, value):
for service in value:
if not SERVICE_RE.match(service):
click.echo(f"Unparsable service '{service}' - must match /^(.+?):(.+)$/")
ctx.exit()
return value
@click.group(
name="phiadm", help="This cli may be used to interact with a local phid instance"
)
@click.option("-d", "--debug", is_flag=True, help="Enable debugging information")
@click.pass_context
def cli(ctx, debug):
ctx.ensure_object(dict)
ctx.obj["debug"] = debug
@cli.command(
"generate",
help="The base name of this LDAP directory (e.g. 'dc=example,dc=com')",
)
@click.option(
"-t",
"--template",
type=click.STRING,
multiple=True,
help=f"Name of the template (allowed: {', '.join(templates)};"
+ " accepts also glob-like patterns)",
)
@click.option(
"-s",
"--default-service",
type=click.STRING,
multiple=True,
callback=validate_services,
help="A pair <name>:<password> representing a service (and the associated password)"
)
@click.option(
"--root-password",
type=click.STRING,
required=True,
help="The cleartext password for the root user",
)
@click.option(
"--phi-password",
type=click.STRING,
required=True,
help="The cleartext password for the phi service",
)
@click.argument(
"base_dn",
type=click.STRING,
)
@click.pass_context
def generate(ctx, template, default_service, root_password, phi_password, base_dn):
config = get_config(base_dn, phi_password, root_password)
debug(ctx, f"default_service: {default_service}")
if default_service:
add_default_services(config, default_service)
debug(ctx, f"config: {config}")
debug(ctx, f"templates: {templates}")
if not template:
template = templates
for name, content in generate_from_templates(config):
debug(ctx, f"current template: {name}")
if any(fnmatch(name, t) for t in template):
click.echo(content)
def get_config(base_dn, phi_password, root_password):
config = {"default_services": []}
config["base_dn"] = base_dn
config["phi_password"] = hash_pass(phi_password)
config["root_password"] = hash_pass(root_password)
return config
def add_default_services(config, services):
for service in services:
user, password = parse_service(service)
config["default_services"].append(
{"name": user, "password": hash_pass(password)})
def parse_service(service):
res = SERVICE_RE.search(service).groups()
if len(res) != 2:
raise ValueError(res)
return res[0], res[1]

View File

@ -0,0 +1,23 @@
version: 1
dn: {{ base_dn }}
objectClass: organization
objectClass: dcObject
dc: unit
o: Unit
dn: ou=Hackers,{{ base_dn }}
objectClass: organizationalUnit
objectClass: top
ou: Hackers
dn: ou=Robots,{{ base_dn }}
objectClass: top
objectClass: organizationalUnit
ou: Robots
dn: ou=Roles,{{ base_dn }}
objectClass: top
objectClass: organizationalUnit
ou: Roles

View File

@ -0,0 +1,17 @@
version: 1
dn: uid=phi,ou=Robots,{{ base_dn }}
objectClass: account
objectClass: simpleSecurityObject
objectClass: top
uid: phi
userPassword: {{ phi_password }}
{%- for service in default_services %}
dn: uid={{ service.name }},ou=Robots,{{ base_dn }}
objectClass: account
objectClass: simpleSecurityObject
objectClass: top
uid={{ service.name }}
userPassword={{ service.password }}
{%- endfor %}

View File

@ -0,0 +1,6 @@
version: 1
dn: cn=Admins,ou=Roles,{{ base_dn }}
cn: Admins
objectClass: groupOfNames
objectClass: top

View File

@ -0,0 +1,14 @@
access to dn.base=""
by * read
by group.exact="cn=Admins,{{ base_dn }}" manage
access to attrs=entry
by * read
access to attrs=userPassword
by self write
by anonymous auth
access to dn.subtree="ou=Hackers,{{ base_dn }}"
by self write
by dn.subtree="ou=Services,{{ base_dn }}" read

View File

View File

@ -0,0 +1,87 @@
#######################################################################
# Modules
#######################################################################
include /etc/openldap/schema/core.schema
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/corba.schema
include /etc/openldap/schema/inetorgperson.schema
include /etc/openldap/schema/nis.schema
include /etc/openldap/schema/collective.schema
include /etc/openldap/schema/openldap.schema
modulepath /usr/lib/openldap
moduleload back_mdb
moduleload refint
moduleload memberof
#######################################################################
# Core
#######################################################################
pidfile /var/slapd/slapd.pid
argsfile /var/slapd/slapd.args
loglevel conns
serverID 0
#######################################################################
# Security
#######################################################################
#TLSCACertificateFile /var/slapd/fullchain.pem
TLSCertificateFile /var/slapd/cert.pem
TLSCertificateKeyFile /var/slapd/key.pem
TLSCipherSuite HIGH
# Sample security restrictions
# Define global ACLs to disable default read access.
# Require integrity protection (prevent hijacking)
# Require 112-bit (3DES or better) encryption for updates
# Require 63-bit encryption for simple bind
security ssf=1 simple_bind=256 update_ssf=256
#######################################################################
# MDB database definitions
#######################################################################
database mdb
maxsize 1073741824
suffix "{{ base_dn }}"
# Overlays to be loaded for the database.
overlay memberof
# Cleartext passwords, especially for the rootdn, should
# be avoid. See slappasswd(8) and slapd.conf(5) for details.
# Use of strong authentication encouraged.
rootdn "cn=root,{{ base_dn }}"
rootpw {{ root_password }}
# The database directory MUST exist prior to running slapd AND
# should only be accessible by the slapd and slap tools.
# Mode 700 recommended.
directory /var/slapd
mode 0700
password-hash {CRYPT}
password-crypt-salt-format "$6$%.16s"
# Indices to maintain
index objectClass pres,eq
index uid,cn,sn,mail eq,sub
index memberof pres,eq
#######################################################################
# MemberOf configuration
#######################################################################
memberof-group-oc groupOfNames
memberof-memberof-ad memberOf
memberof-member-ad member
memberof-dangling error
memberof-refint true

25
src/phi/cli/utils.py Normal file
View File

@ -0,0 +1,25 @@
# -*- encoding: utf-8 -*-
import os.path
from jinja2 import Environment, ChoiceLoader, FileSystemLoader, PackageLoader
def get_jinja_env():
pkg_path_templates = os.path.realpath(os.path.join(__name__, "./templates"))
loaders = [
FileSystemLoader(pkg_path_templates),
PackageLoader("phi.cli", package_path="templates"),
]
return Environment(loader=ChoiceLoader(loaders))
def generate_from_templates(config):
env = get_jinja_env()
for t in env.list_templates(extensions="j2"):
tmpl = env.get_template(t)
content = tmpl.render(**config)
# This is loaded from a resource and won't be None
name = tmpl.name.strip(".j2")
# name = os.path.basename(tmpl.filename).strip(".j2")
yield (name, content)