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",
|
||||
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
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