Add phiadm
This commit is contained in:
parent
fa7b633a16
commit
b376d1f270
4
setup.py
4
setup.py
|
@ -10,7 +10,9 @@ setup(
|
||||||
author_email="unit@paranoici.org",
|
author_email="unit@paranoici.org",
|
||||||
package_dir={"": "src"},
|
package_dir={"": "src"},
|
||||||
packages=find_packages("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"],
|
setup_requires=["pytest-runner"],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"aiohttp==3.8.1",
|
"aiohttp==3.8.1",
|
||||||
|
|
0
src/phi/cli/__init__.py
Normal file
0
src/phi/cli/__init__.py
Normal file
119
src/phi/cli/adm.py
Normal file
119
src/phi/cli/adm.py
Normal 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]
|
23
src/phi/cli/templates/00-base.ldif.j2
Normal file
23
src/phi/cli/templates/00-base.ldif.j2
Normal 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
|
||||||
|
|
17
src/phi/cli/templates/10-users.ldif.j2
Normal file
17
src/phi/cli/templates/10-users.ldif.j2
Normal 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 %}
|
6
src/phi/cli/templates/20-roles.ldif.j2
Normal file
6
src/phi/cli/templates/20-roles.ldif.j2
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
dn: cn=Admins,ou=Roles,{{ base_dn }}
|
||||||
|
cn: Admins
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
14
src/phi/cli/templates/99-acl.ldif.j2
Normal file
14
src/phi/cli/templates/99-acl.ldif.j2
Normal 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
|
0
src/phi/cli/templates/__init__.py
Normal file
0
src/phi/cli/templates/__init__.py
Normal file
87
src/phi/cli/templates/slapd.conf.j2
Normal file
87
src/phi/cli/templates/slapd.conf.j2
Normal 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
25
src/phi/cli/utils.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user