feat(active-directory): LDAP directory administration integration
Active Directory over LDAP/LDAPS via the ldap3 library, designed to run on a remote engine inside the AD network. 24 commands: raw search, get user/computer/group-members (with userAccountControl decoding), enable/disable/ unlock accounts, set/expire password, password-never-expire, add/remove group membership, move user/computer OU, create/update/delete user/contact/group, and credential testing. Scripts share an ldap3 connection helper that handles SSL/LDAPS/Start TLS, NTLM bind, certificate trust and paged search. ldap3 is imported defensively: if it is missing on the engine host the command returns a clear "pip install ldap3" message instead of crashing. No ingestion source, so no OCSF mapper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
id: active_directory
|
||||
name: Active Directory
|
||||
version: 1.0.0
|
||||
description: "Active Directory over LDAP/LDAPS — query and manage users, computers, groups and contacts (search, enable/disable/unlock, set/expire password, group membership, OU moves, create/update/delete). Runs on a remote engine inside the AD network. Requires the Python 'ldap3' library on the engine host (pip install ldap3)."
|
||||
changelog: "1.0.0 — Initial release: full directory administration over LDAP (search, get user/computer/group-members, create/update/delete user/contact/group, enable/disable/unlock, set/expire password, password-never-expire, add/remove group membership, move user/computer OU, test credentials)."
|
||||
category: identity
|
||||
|
||||
# Per-instance configuration. The integration binds to a Domain Controller over
|
||||
# LDAP (389) or LDAPS (636). Secure connection (SSL/Start TLS) is required for
|
||||
# password operations. Run this integration on a remote engine that can reach the
|
||||
# Domain Controller. The bundled scripts require the Python 'ldap3' library to be
|
||||
# installed on the engine host: pip install ldap3
|
||||
config_schema:
|
||||
properties:
|
||||
server_ip:
|
||||
type: string
|
||||
description: "Domain Controller host or IP (e.g. dc01.company.com or 192.168.0.1)"
|
||||
port:
|
||||
type: string
|
||||
description: "LDAP port. Default 389 for LDAP / Start TLS, 636 for LDAPS."
|
||||
username:
|
||||
type: string
|
||||
description: "Bind username (e.g. DOMAIN\\\\user or user@company.com)"
|
||||
password:
|
||||
type: string
|
||||
description: "Bind password"
|
||||
x-soar-sensitive: true
|
||||
base_dn:
|
||||
type: string
|
||||
description: "Base DN (e.g. dc=company,dc=com)"
|
||||
secure_connection:
|
||||
type: string
|
||||
description: "Connection security: None, SSL (LDAPS), TLS, or Start TLS. Password operations require SSL or TLS."
|
||||
default: SSL
|
||||
ssl_version:
|
||||
type: string
|
||||
description: "SSL/TLS protocol: None, TLS, TLSv1, TLSv1_1, TLSv1_2, or TLS_CLIENT. Default None (let the library negotiate)."
|
||||
default: None
|
||||
ntlm:
|
||||
type: boolean
|
||||
description: "Use NTLM authentication for the bind"
|
||||
default: false
|
||||
unsecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
page_size:
|
||||
type: string
|
||||
description: "LDAP paging size for searches"
|
||||
default: "500"
|
||||
default_base_query:
|
||||
type: string
|
||||
description: "Default LDAP filter used by ad-get-user when no query argument is given"
|
||||
default: "(&(objectClass=User)(objectCategory=person))"
|
||||
required:
|
||||
- server_ip
|
||||
- username
|
||||
- password
|
||||
- base_dn
|
||||
|
||||
commands:
|
||||
# ── Connectivity ──────────────────────────────────────────────────────────
|
||||
- id: test_connection
|
||||
name: ad-test-connection
|
||||
description: "Bind to the Domain Controller and verify the configured base DN (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Search / read ─────────────────────────────────────────────────────────
|
||||
- id: ad_search
|
||||
name: ad-search
|
||||
description: "Run a raw LDAP query and return the matching entries. Paging is handled internally up to size-limit."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
filter: { type: string, description: "LDAP search filter, e.g. (&(objectCategory=person)(objectClass=user))" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
attributes: { type: string, description: "CSV list of attributes to return, or ALL for every attribute" }
|
||||
size-limit: { type: number, description: "Maximum entries to return (default 50)" }
|
||||
time-limit: { type: number, description: "Maximum search time in seconds" }
|
||||
page-size: { type: number, description: "Paging size (overrides size-limit when set)" }
|
||||
required: [filter]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_get_user
|
||||
name: ad-get-user
|
||||
description: "Retrieve user accounts by DN, name, email, sAMAccountName or a custom attribute. Decodes userAccountControl flags."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
dn: { type: string, description: "Distinguished Name of the user" }
|
||||
name: { type: string, description: "Common name (cn) of the user" }
|
||||
email: { type: string, description: "User email (mail)" }
|
||||
username: { type: string, description: "sAMAccountName" }
|
||||
sAMAccountName: { type: string, description: "sAMAccountName (alias of username)" }
|
||||
custom-field-type: { type: string, description: "Attribute name to query by" }
|
||||
custom-field-data: { type: string, description: "Attribute value (required with custom-field-type)" }
|
||||
attributes: { type: string, description: "CSV list of extra attributes to add to the defaults" }
|
||||
attributes-to-exclude: { type: string, description: "CSV list of attributes to remove from the result" }
|
||||
limit: { type: number, description: "Maximum users to return (default 20)" }
|
||||
page-size: { type: number, description: "Paging size (overrides limit when set)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_get_computer
|
||||
name: ad-get-computer
|
||||
description: "Retrieve computer accounts by DN, name or a custom attribute."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
dn: { type: string, description: "Distinguished Name of the computer" }
|
||||
name: { type: string, description: "Computer name" }
|
||||
custom-field-type: { type: string, description: "Attribute name to query by" }
|
||||
custom-field-data: { type: string, description: "Attribute value (required with custom-field-type)" }
|
||||
attributes: { type: string, description: "CSV list of extra attributes to add to the defaults" }
|
||||
limit: { type: number, description: "Maximum computers to return" }
|
||||
page-size: { type: number, description: "Paging size (overrides limit when set)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_get_group_members
|
||||
name: ad-get-group-members
|
||||
description: "List the users, computers or nested groups that are members of a group (recursive by default)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
group-dn: { type: string, description: "Distinguished Name of the group" }
|
||||
member-type: { type: string, description: "Member type to return: person, computer or group (default person)" }
|
||||
attributes: { type: string, description: "CSV list of extra attributes to add to the defaults" }
|
||||
time_limit: { type: number, description: "Search time limit in seconds (default 180)" }
|
||||
disable-nested-search: { type: string, description: "Set true to disable recursive membership resolution (default false)" }
|
||||
sAMAccountName: { type: string, description: "Filter members by sAMAccountName (default *)" }
|
||||
limit: { type: number, description: "Maximum members to return" }
|
||||
page-size: { type: number, description: "Paging size (overrides limit when set)" }
|
||||
required: [group-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Account state ─────────────────────────────────────────────────────────
|
||||
- id: ad_enable_account
|
||||
name: ad-enable-account
|
||||
description: "Enable a previously disabled user account."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the account to enable" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
restore_user: { type: string, description: "Set true to restore the account's previous userAccountControl flags" }
|
||||
required: [username]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_disable_account
|
||||
name: ad-disable-account
|
||||
description: "Disable a user account."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the account to disable" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [username]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_unlock_account
|
||||
name: ad-unlock-account
|
||||
description: "Unlock a locked-out user account."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the account to unlock" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [username]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_set_new_password
|
||||
name: ad-set-new-password
|
||||
description: "Set a new password for a user. Requires a secure connection (SSL or TLS)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName whose password will be set" }
|
||||
password: { type: string, description: "New password", x-soar-sensitive: true }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [username, password]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_expire_password
|
||||
name: ad-expire-password
|
||||
description: "Force a user to change their password at next login."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the user" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [username]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_modify_password_never_expire
|
||||
name: ad-modify-password-never-expire
|
||||
description: "Set or clear the 'Password Never Expire' flag on a user account."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the user" }
|
||||
value: { type: string, description: "true to set 'Password Never Expire', false to clear it" }
|
||||
required: [username, value]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Group membership ──────────────────────────────────────────────────────
|
||||
- id: ad_add_to_group
|
||||
name: ad-add-to-group
|
||||
description: "Add a user, computer or nested group to a group."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "Username(s) to add (single or CSV). Mutually exclusive with computer-name." }
|
||||
computer-name: { type: string, description: "Computer name(s) to add (single or CSV)" }
|
||||
nested_group_cn: { type: string, description: "A group CN to add as a nested member" }
|
||||
group-cn: { type: string, description: "Target group CN" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [group-cn]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_remove_from_group
|
||||
name: ad-remove-from-group
|
||||
description: "Remove a user or computer from a group."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "Username to remove. Mutually exclusive with computer-name." }
|
||||
computer-name: { type: string, description: "Computer name to remove" }
|
||||
group-cn: { type: string, description: "Target group CN" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [group-cn]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Organizational unit ───────────────────────────────────────────────────
|
||||
- id: ad_modify_user_ou
|
||||
name: ad-modify-user-ou
|
||||
description: "Move a user to a different organizational unit within the domain."
|
||||
inputs_schema:
|
||||
properties:
|
||||
user-name: { type: string, description: "Name of the user to move" }
|
||||
full-superior-dn: { type: string, description: "Target OU DN, e.g. OU=users,DC=domain,DC=com" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [user-name, full-superior-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_modify_computer_ou
|
||||
name: ad-modify-computer-ou
|
||||
description: "Move a computer to a different organizational unit within the domain."
|
||||
inputs_schema:
|
||||
properties:
|
||||
computer-name: { type: string, description: "Name of the computer to move" }
|
||||
full-superior-dn: { type: string, description: "Target OU DN, e.g. OU=computers,DC=domain,DC=com" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [computer-name, full-superior-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Object lifecycle: users ───────────────────────────────────────────────
|
||||
- id: ad_create_user
|
||||
name: ad-create-user
|
||||
description: "Create a user account, set its initial password and enable it. Requires a secure connection (SSL or TLS)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName for the new user" }
|
||||
password: { type: string, description: "Initial password (user must change at next login)", x-soar-sensitive: true }
|
||||
user-dn: { type: string, description: "Full DN of the new user" }
|
||||
display-name: { type: string, description: "Display name" }
|
||||
description: { type: string, description: "Description" }
|
||||
email: { type: string, description: "Email (mail)" }
|
||||
telephone-number: { type: string, description: "Telephone number" }
|
||||
title: { type: string, description: "Job title" }
|
||||
custom-attributes: { type: string, description: "JSON object of extra attributes, e.g. {\"company\":\"ACME\"}" }
|
||||
required: [username, password, user-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_update_user
|
||||
name: ad-update-user
|
||||
description: "Replace a single attribute on an existing user."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the user" }
|
||||
attribute-name: { type: string, description: "Attribute to modify (e.g. sn, displayName, mail)" }
|
||||
attribute-value: { type: string, description: "New value" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [username, attribute-name, attribute-value]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_delete_user
|
||||
name: ad-delete-user
|
||||
description: "Delete a user (or any object) by its DN."
|
||||
inputs_schema:
|
||||
properties:
|
||||
user-dn: { type: string, description: "DN of the object to delete" }
|
||||
required: [user-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Object lifecycle: contacts ────────────────────────────────────────────
|
||||
- id: ad_create_contact
|
||||
name: ad-create-contact
|
||||
description: "Create a contact object."
|
||||
inputs_schema:
|
||||
properties:
|
||||
contact-dn: { type: string, description: "Full DN of the new contact" }
|
||||
display-name: { type: string, description: "Display name" }
|
||||
description: { type: string, description: "Description" }
|
||||
email: { type: string, description: "Email (mail)" }
|
||||
telephone-number: { type: string, description: "Telephone number" }
|
||||
title: { type: string, description: "Job title" }
|
||||
custom-attributes: { type: string, description: "JSON object of extra attributes" }
|
||||
required: [contact-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_update_contact
|
||||
name: ad-update-contact
|
||||
description: "Replace a single attribute on an existing contact."
|
||||
inputs_schema:
|
||||
properties:
|
||||
contact-dn: { type: string, description: "DN of the contact" }
|
||||
attribute-name: { type: string, description: "Attribute to modify" }
|
||||
attribute-value: { type: string, description: "New value" }
|
||||
required: [contact-dn, attribute-name, attribute-value]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Object lifecycle: groups ──────────────────────────────────────────────
|
||||
- id: ad_create_group
|
||||
name: ad-create-group
|
||||
description: "Create a security or distribution group."
|
||||
inputs_schema:
|
||||
properties:
|
||||
name: { type: string, description: "Group name (sAMAccountName)" }
|
||||
group-type: { type: string, description: "security or distribution" }
|
||||
dn: { type: string, description: "Full DN of the new group" }
|
||||
members: { type: array, description: "DNs of initial members" }
|
||||
required: [name, group-type, dn]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_update_group
|
||||
name: ad-update-group
|
||||
description: "Replace a single attribute on an existing group."
|
||||
inputs_schema:
|
||||
properties:
|
||||
groupname: { type: string, description: "Group name (cn) to update" }
|
||||
attributename: { type: string, description: "Attribute to modify (e.g. description, displayName)" }
|
||||
attributevalue: { type: string, description: "New value" }
|
||||
basedn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [attributename, attributevalue]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_delete_group
|
||||
name: ad-delete-group
|
||||
description: "Delete a security or distribution group by its DN."
|
||||
inputs_schema:
|
||||
properties:
|
||||
dn: { type: string, description: "DN of the group" }
|
||||
required: [dn]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Credentials ───────────────────────────────────────────────────────────
|
||||
- id: ad_test_credentials
|
||||
name: ad-test-credentials
|
||||
description: "Test whether a username/password can bind to the Domain Controller."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "Username to test (user or SERVER\\\\user)" }
|
||||
password: { type: string, description: "Password to test", x-soar-sensitive: true }
|
||||
required: [username, password]
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,240 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
sb = base_dn_arg()
|
||||
if I.get("username") and I.get("computer-name"):
|
||||
fail("Provide either username or computer-name, not both")
|
||||
member_dns = []
|
||||
if I.get("username"):
|
||||
member_dns = [user_dn(conn, u, sb) for u in _list(I["username"])]
|
||||
elif I.get("computer-name"):
|
||||
member_dns = [computer_dn(conn, cn, sb) for cn in _list(I["computer-name"])]
|
||||
elif I.get("nested_group_cn"):
|
||||
member_dns = [group_dn(conn, I["nested_group_cn"], sb)]
|
||||
else:
|
||||
fail("Provide username, computer-name, or nested_group_cn")
|
||||
grp = group_dn(conn, I["group-cn"], sb)
|
||||
if not microsoft.addMembersToGroups.ad_add_members_to_groups(conn, member_dns, [grp], True):
|
||||
fail("Failed to add member(s) to group " + I["group-cn"])
|
||||
out({"added": member_dns, "group": I["group-cn"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,239 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
contact_dn = I["contact-dn"]
|
||||
attributes = {}
|
||||
if I.get("custom-attributes"):
|
||||
try:
|
||||
attributes = json.loads(I["custom-attributes"])
|
||||
except Exception as e:
|
||||
fail("Failed to parse custom-attributes JSON: " + str(e))
|
||||
field_map = {"display-name": "displayName", "description": "description", "email": "mail",
|
||||
"telephone-number": "telephoneNumber", "title": "title"}
|
||||
for arg, attr in field_map.items():
|
||||
if I.get(arg):
|
||||
attributes[attr] = I[arg]
|
||||
if not conn.add(contact_dn, ["top", "person", "organizationalPerson", "contact"], attributes):
|
||||
fail("Failed to create contact: " + json.dumps(conn.result, default=str))
|
||||
out({"created": contact_dn})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,235 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = I["dn"]
|
||||
name = I["name"]
|
||||
type_map = {"security": "2147483650", "distribution": "2"}
|
||||
if I["group-type"] not in type_map:
|
||||
fail("group-type must be 'security' or 'distribution'")
|
||||
attributes = {"samAccountName": name, "groupType": type_map[I["group-type"]]}
|
||||
if I.get("members"):
|
||||
attributes["member"] = _list(I["members"])
|
||||
if not conn.add(dn, ["top", "group"], attributes):
|
||||
fail("Failed to create group: " + json.dumps(conn.result, default=str))
|
||||
out({"created": dn})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,246 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
if c["secure"] not in ("SSL", "TLS"):
|
||||
fail("Creating a user requires a secure connection (SSL or TLS).")
|
||||
user_dn_val = I["user-dn"]
|
||||
object_classes = ["top", "person", "organizationalPerson", "user"]
|
||||
attributes = {"sAMAccountName": I["username"]}
|
||||
field_map = {"display-name": "displayName", "description": "description", "email": "mail",
|
||||
"telephone-number": "telephoneNumber", "title": "title"}
|
||||
for arg, attr in field_map.items():
|
||||
if I.get(arg):
|
||||
attributes[attr] = I[arg]
|
||||
if I.get("custom-attributes"):
|
||||
try:
|
||||
attributes.update(json.loads(I["custom-attributes"]))
|
||||
except Exception as e:
|
||||
fail("Failed to parse custom-attributes JSON: " + str(e))
|
||||
if not conn.add(user_dn_val, object_classes, attributes):
|
||||
fail("Failed to create user: " + json.dumps(conn.result, default=str))
|
||||
if not conn.extend.microsoft.modify_password(user_dn_val, I["password"]):
|
||||
fail("User created but failed to set password: " + json.dumps(conn.result, default=str))
|
||||
modify(conn, user_dn_val, {"userAccountControl": [(MODIFY_REPLACE, [512])],
|
||||
"pwdLastSet": [(MODIFY_REPLACE, ["0"])]})
|
||||
out({"created": user_dn_val})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,227 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
if not conn.delete(I["dn"]):
|
||||
fail("Failed to delete group: " + json.dumps(conn.result, default=str))
|
||||
out({"deleted": I["dn"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,228 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = I["user-dn"]
|
||||
if not conn.delete(dn):
|
||||
fail("Failed to delete: " + json.dumps(conn.result, default=str))
|
||||
out({"deleted": dn})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,229 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
sb = I.get("base-dn") or base_dn_arg()
|
||||
dn = user_dn(conn, I["username"], sb)
|
||||
options = current_uac(conn, sb)
|
||||
modify(conn, dn, {"userAccountControl": [(MODIFY_REPLACE, [options | 0x2])]})
|
||||
out({"disabled": I["username"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,232 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
sb = I.get("base-dn") or base_dn_arg()
|
||||
dn = user_dn(conn, I["username"], sb)
|
||||
options = 512
|
||||
if str(I.get("restore_user")).lower() == "true":
|
||||
options = current_uac(conn, sb)
|
||||
options = options & ~0x2
|
||||
modify(conn, dn, {"userAccountControl": [(MODIFY_REPLACE, [options])]})
|
||||
out({"enabled": I["username"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,227 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = user_dn(conn, I["username"], I.get("base-dn") or base_dn_arg())
|
||||
modify(conn, dn, {"pwdLastSet": [(MODIFY_REPLACE, ["0"])]})
|
||||
out({"expired": I["username"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,243 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
defaults = ["name", "memberOf"]
|
||||
query = "(&(objectClass=user)(objectCategory=computer))"
|
||||
esc = escape_filter_chars
|
||||
if I.get("dn"):
|
||||
query = f"(&(objectClass=user)(objectCategory=computer)(distinguishedName={esc(I['dn'])}))"
|
||||
elif I.get("name"):
|
||||
query = f"(&(objectClass=user)(objectCategory=computer)(name={esc(I['name'])}))"
|
||||
elif I.get("custom-field-type"):
|
||||
if not I.get("custom-field-data"):
|
||||
fail("custom-field-data is required when custom-field-type is given")
|
||||
query = f"(&(objectClass=user)(objectCategory=computer)({esc(I['custom-field-type'])}={esc(I['custom-field-data'])}))"
|
||||
custom = I["attributes"].split(",") if I.get("attributes") else []
|
||||
attributes = list(set(custom) | set(defaults))
|
||||
size_limit = int(I.get("limit") or 0)
|
||||
page_size = int(I.get("page-size") or c["page_size"])
|
||||
if I.get("page-size"):
|
||||
size_limit = page_size
|
||||
flat = search_paging(conn, query, base_dn_arg(), attributes=attributes, page_size=page_size, size_limit=size_limit)
|
||||
out({"computers": flat, "count": len(flat)})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,248 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
member_type = I.get("member-type") or "person"
|
||||
group = I["group-dn"]
|
||||
nested = "" if str(I.get("disable-nested-search")).lower() == "true" else ":1.2.840.113556.1.4.1941:"
|
||||
account = I.get("sAMAccountName") or "*"
|
||||
time_limit = int(I.get("time_limit") or 180)
|
||||
defaults = {
|
||||
"person": ["name", "displayName", "memberOf", "mail", "sAMAccountName", "manager", "userAccountControl"],
|
||||
"group": ["name", "memberOf"],
|
||||
"computer": ["name", "memberOf"],
|
||||
}.get(member_type, ["name", "memberOf"])
|
||||
custom = I["attributes"].split(",") if I.get("attributes") else []
|
||||
attributes = list(set(custom) | set(defaults))
|
||||
if member_type == "group":
|
||||
query = f"(&(objectCategory={member_type})(memberOf{nested}={group})(sAMAccountName={account}))"
|
||||
else:
|
||||
query = f"(&(objectCategory={member_type})(objectClass=user)(memberOf{nested}={group})(sAMAccountName={account}))"
|
||||
size_limit = int(I.get("limit") or 0)
|
||||
page_size = int(I.get("page-size") or c["page_size"])
|
||||
if I.get("page-size"):
|
||||
size_limit = page_size
|
||||
flat = search_paging(conn, query, base_dn_arg(), attributes=attributes,
|
||||
page_size=page_size, size_limit=size_limit, time_limit=time_limit)
|
||||
members = [{"dn": e["dn"], "category": member_type} for e in flat]
|
||||
out({"group_dn": group, "members": members, "entries": flat, "count": len(flat)})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,254 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
defaults = ["name", "displayName", "memberOf", "mail", "sAMAccountName", "manager", "userAccountControl"]
|
||||
query = S.get("default_base_query") or "(&(objectClass=User)(objectCategory=person))"
|
||||
esc = escape_filter_chars
|
||||
if I.get("dn"):
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(distinguishedName={esc(I['dn'])}))"
|
||||
elif I.get("name"):
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(cn={esc(I['name'])}))"
|
||||
elif I.get("email"):
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(mail={esc(I['email'])}))"
|
||||
elif I.get("username") or I.get("sAMAccountName"):
|
||||
u = esc(I.get("username") or I.get("sAMAccountName"))
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={u}))"
|
||||
elif I.get("custom-field-type"):
|
||||
if not I.get("custom-field-data"):
|
||||
fail("custom-field-data is required when custom-field-type is given")
|
||||
query = f"(&(objectClass=User)(objectCategory=person)({esc(I['custom-field-type'])}={esc(I['custom-field-data'])}))"
|
||||
custom = I["attributes"].split(",") if I.get("attributes") else []
|
||||
exclude = [a.strip() for a in (I.get("attributes-to-exclude") or "").split(",") if a.strip()]
|
||||
attributes = list((set(custom) | set(defaults)) - set(exclude))
|
||||
limit = int(I.get("limit") or 20)
|
||||
if limit <= 0:
|
||||
limit = 20
|
||||
page_size = int(I.get("page-size") or c["page_size"])
|
||||
if I.get("page-size"):
|
||||
limit = int(I["page-size"])
|
||||
flat = search_paging(conn, query, base_dn_arg(), attributes=attributes, size_limit=limit, page_size=page_size)
|
||||
for user in flat:
|
||||
if user.get("userAccountControl"):
|
||||
user["userAccountControlFields"] = uac_fields(first_uac(user["userAccountControl"]))
|
||||
out({"users": flat, "count": len(flat)})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,229 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
name = I["computer-name"]
|
||||
dn = computer_dn(conn, name, I.get("base-dn") or base_dn_arg())
|
||||
if not conn.modify_dn(dn, f"CN={name}", new_superior=I["full-superior-dn"]):
|
||||
fail("Failed to move computer: " + json.dumps(conn.result, default=str))
|
||||
out({"moved": name, "to": I["full-superior-dn"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,236 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
sam = I["username"]
|
||||
never = str(I["value"]).lower() == "true"
|
||||
sb = base_dn_arg()
|
||||
flat = search_paging(conn, f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
sb, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if not flat:
|
||||
fail(f"User {sam} not found")
|
||||
uac = first_uac(flat[0].get("userAccountControl"))
|
||||
uac = (uac | (1 << 16)) if never else (uac & ~(1 << 16))
|
||||
dn = user_dn(conn, sam, sb)
|
||||
modify(conn, dn, {"userAccountControl": [(MODIFY_REPLACE, [uac])]})
|
||||
out({"username": sam, "password_never_expire": never})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,231 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
name = I["user-name"]
|
||||
dn = user_dn(conn, name, I.get("base-dn") or base_dn_arg())
|
||||
cn = dn.split(",OU=", 1)[0].split(",DC=", 1)[0].replace("\\", "")
|
||||
dn_clean = dn.replace("\\", "")
|
||||
if not conn.modify_dn(dn_clean, cn, new_superior=I["full-superior-dn"]):
|
||||
fail("Failed to move user: " + json.dumps(conn.result, default=str))
|
||||
out({"moved": name, "to": I["full-superior-dn"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,237 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
sb = base_dn_arg()
|
||||
if I.get("username") and I.get("computer-name"):
|
||||
fail("Provide either username or computer-name, not both")
|
||||
if I.get("username"):
|
||||
member_dn = user_dn(conn, I["username"], sb)
|
||||
elif I.get("computer-name"):
|
||||
member_dn = computer_dn(conn, I["computer-name"], sb)
|
||||
else:
|
||||
fail("Provide username or computer-name")
|
||||
grp = group_dn(conn, I["group-cn"], sb)
|
||||
if not microsoft.removeMembersFromGroups.ad_remove_members_from_groups(conn, [member_dn], [grp], True):
|
||||
fail("Failed to remove member from group " + I["group-cn"])
|
||||
out({"removed": member_dn, "group": I["group-cn"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,239 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
search_filter = I["filter"]
|
||||
for a, b in {"\(": "\28", "\)": "\29", "\*": "\2a", "\/": "\2f", "\\\\": "\5c"}.items():
|
||||
search_filter = search_filter.replace(a, b)
|
||||
search_base = base_dn_arg()
|
||||
attributes = I.get("attributes")
|
||||
if attributes:
|
||||
attributes = ALL_ATTRIBUTES if attributes == "ALL" else [a.strip() for a in attributes.split(",")]
|
||||
size_limit = int(I.get("size-limit") or 50)
|
||||
time_limit = int(I.get("time-limit") or 0)
|
||||
page_size = int(I.get("page-size") or c["page_size"])
|
||||
if I.get("page-size"):
|
||||
size_limit = page_size
|
||||
flat = search_paging(conn, search_filter, search_base, attributes=attributes,
|
||||
page_size=page_size, size_limit=size_limit, time_limit=time_limit)
|
||||
out({"results": flat, "count": len(flat)})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,230 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
if c["secure"] not in ("SSL", "TLS"):
|
||||
fail("Setting a password requires a secure connection (SSL or TLS).")
|
||||
dn = user_dn(conn, I["username"], I.get("base-dn") or base_dn_arg())
|
||||
if not conn.extend.microsoft.modify_password(dn, I["password"]):
|
||||
fail("Failed to set password: " + json.dumps(conn.result, default=str))
|
||||
out({"password_set": I["username"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,236 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
server = _server(c)
|
||||
username = I["username"]
|
||||
try:
|
||||
if c["ntlm"]:
|
||||
domain_user = username if "\\" in username else c["host"] + "\\" + username
|
||||
test = Connection(server, domain_user, password=I["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
test = Connection(server, user=username, password=I["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
test.unbind()
|
||||
except LDAPException:
|
||||
fail(f"Credential test for {username} failed")
|
||||
out({"valid": username})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,228 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = user_dn(conn, I["username"], I.get("base-dn") or base_dn_arg())
|
||||
if not microsoft.unlockAccount.ad_unlock_account(conn, dn):
|
||||
fail("Failed to unlock " + I["username"])
|
||||
out({"unlocked": I["username"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,226 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
modify(conn, I["contact-dn"], {I["attribute-name"]: [(MODIFY_REPLACE, [I["attribute-value"]])]})
|
||||
out({"updated": I["contact-dn"], "attribute": I["attribute-name"], "value": I["attribute-value"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,227 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = group_dn(conn, I["groupname"], I.get("basedn") or base_dn_arg())
|
||||
modify(conn, dn, {I["attributename"]: [(MODIFY_REPLACE, [I["attributevalue"]])]})
|
||||
out({"updated": dn, "attribute": I["attributename"], "value": I["attributevalue"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,227 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = user_dn(conn, I["username"], I.get("base-dn") or base_dn_arg())
|
||||
modify(conn, dn, {I["attribute-name"]: [(MODIFY_REPLACE, [I["attribute-value"]])]})
|
||||
out({"updated": dn, "attribute": I["attribute-name"], "value": I["attribute-value"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,229 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
if c["base_dn"]:
|
||||
found = conn.search(search_base=c["base_dn"], search_filter="(objectClass=*)",
|
||||
search_scope=BASE, size_limit=1, attributes=["1.1"])
|
||||
if not found:
|
||||
fail("Connected, but failed to verify base DN: " + json.dumps(conn.result, default=str))
|
||||
out({"ok": True})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user