Compare commits
3 Commits
e88805ccab
...
9da00ad7b9
| Author | SHA1 | Date | |
|---|---|---|---|
| 9da00ad7b9 | |||
| 7bd6ad6a69 | |||
| 26694b512a |
@@ -0,0 +1,140 @@
|
|||||||
|
id: cyberark_pas
|
||||||
|
name: CyberArk PAS
|
||||||
|
version: 1.0.0
|
||||||
|
description: "CyberArk Privileged Access Security (PVWA REST API) — PAM containment and IR: list and read privileged accounts, rotate/verify/reconcile credentials (CPM), retrieve a password for incident response, add/delete accounts, and list safes and vault users. Session (logon) authentication; stdlib-only, no extra Python dependencies."
|
||||||
|
changelog: "1.0.0 — Initial release: list/get accounts, change/verify/reconcile credentials, retrieve password, add/delete account, list safes, list users."
|
||||||
|
category: identity
|
||||||
|
|
||||||
|
# Per-instance configuration. Each command logs on to PVWA (returns a session
|
||||||
|
# token used as the Authorization header), performs the action, then logs off.
|
||||||
|
config_schema:
|
||||||
|
properties:
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
description: "PVWA base URL (e.g. https://pvwa.example.com)"
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: "Vault username"
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
description: "Vault password"
|
||||||
|
x-soar-sensitive: true
|
||||||
|
auth_method:
|
||||||
|
type: string
|
||||||
|
description: "Logon method: Cyberark, LDAP, RADIUS, or Windows (default Cyberark)"
|
||||||
|
default: "Cyberark"
|
||||||
|
insecure:
|
||||||
|
type: boolean
|
||||||
|
description: "Trust any TLS certificate (not secure)"
|
||||||
|
default: false
|
||||||
|
required:
|
||||||
|
- url
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
|
||||||
|
commands:
|
||||||
|
- id: list_accounts
|
||||||
|
name: cyberark-list-accounts
|
||||||
|
description: "List/search privileged accounts."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
search: { type: string, description: "Free-text search (username, address, name)" }
|
||||||
|
filter: { type: string, description: "Filter expression (e.g. safeName eq MySafe)" }
|
||||||
|
limit: { type: number, description: "Max accounts (default 50)" }
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: get_account_details
|
||||||
|
name: cyberark-get-account-details
|
||||||
|
description: "Get a single account's details by account ID."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
account_id: { type: string, description: "Account ID" }
|
||||||
|
required: [account_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: change_credentials
|
||||||
|
name: cyberark-change-credentials
|
||||||
|
description: "Rotate an account's credential immediately to a new random password (CPM change). Use to contain a compromised privileged account."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
account_id: { type: string, description: "Account ID" }
|
||||||
|
change_entire_group: { type: boolean, description: "Also change dependent accounts in the group (default false)" }
|
||||||
|
required: [account_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: verify_credentials
|
||||||
|
name: cyberark-verify-credentials
|
||||||
|
description: "Mark an account's credential for verification by the CPM."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
account_id: { type: string, description: "Account ID" }
|
||||||
|
required: [account_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: reconcile_credentials
|
||||||
|
name: cyberark-reconcile-credentials
|
||||||
|
description: "Mark an account's credential for reconciliation by the CPM."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
account_id: { type: string, description: "Account ID" }
|
||||||
|
required: [account_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: get_password
|
||||||
|
name: cyberark-get-password
|
||||||
|
description: "Retrieve an account's current password (for incident response). Requires a reason."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
account_id: { type: string, description: "Account ID" }
|
||||||
|
reason: { type: string, description: "Reason for retrieval (audited)" }
|
||||||
|
required: [account_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: add_account
|
||||||
|
name: cyberark-add-account
|
||||||
|
description: "Add a new privileged account to a safe."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
name: { type: string, description: "Account name" }
|
||||||
|
address: { type: string, description: "Target machine address" }
|
||||||
|
user_name: { type: string, description: "Account username" }
|
||||||
|
platform_id: { type: string, description: "Platform ID (e.g. WinServerLocal)" }
|
||||||
|
safe_name: { type: string, description: "Safe name" }
|
||||||
|
secret: { type: string, description: "The password/secret to store" }
|
||||||
|
secret_type: { type: string, description: "password or key (default password)" }
|
||||||
|
required: [name, address, user_name, platform_id, safe_name]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: delete_account
|
||||||
|
name: cyberark-delete-account
|
||||||
|
description: "Delete an account by ID."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
account_id: { type: string, description: "Account ID" }
|
||||||
|
required: [account_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: list_safes
|
||||||
|
name: cyberark-list-safes
|
||||||
|
description: "List safes."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
search: { type: string, description: "Optional safe-name search" }
|
||||||
|
limit: { type: number, description: "Max safes (default 50)" }
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: list_users
|
||||||
|
name: cyberark-list-users
|
||||||
|
description: "List vault users."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
search: { type: string, description: "Optional username search" }
|
||||||
|
filter: { type: string, description: "Filter (e.g. userType eq EPVUser)" }
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
|
||||||
|
- id: test_connection
|
||||||
|
name: cyberark-test-connection
|
||||||
|
description: "Verify connectivity and logon credentials (used by the Test button)."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(cfg):
|
||||||
|
if cfg.get("insecure"):
|
||||||
|
c = ssl.create_default_context()
|
||||||
|
c.check_hostname = False
|
||||||
|
c.verify_mode = ssl.CERT_NONE
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.base = str(cfg.get("url", "")).rstrip("/") + "/PasswordVault"
|
||||||
|
self.ctx = _ctx(cfg)
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _open(self, method, path, body=None, params=None, auth=True):
|
||||||
|
url = self.base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if auth and self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def logon(self):
|
||||||
|
method = str(self.cfg.get("auth_method") or "Cyberark").strip() or "Cyberark"
|
||||||
|
body = {
|
||||||
|
"username": self.cfg.get("username", ""),
|
||||||
|
"password": self.cfg.get("password", ""),
|
||||||
|
"concurrentSession": True,
|
||||||
|
}
|
||||||
|
tok = self._open("POST", "/API/auth/" + urllib.parse.quote(method, safe="") + "/Logon", body=body, auth=False)
|
||||||
|
# tok is the session token (a plain string when parsed from the JSON body)
|
||||||
|
self.token = tok if isinstance(tok, str) else str(tok)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
try:
|
||||||
|
self._open("POST", "/API/Auth/Logoff", body={})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call(self, method, path, body=None, params=None):
|
||||||
|
return self._open(method, path, body=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
client = Client(cfg)
|
||||||
|
client.logon()
|
||||||
|
try:
|
||||||
|
result = fn(client, inputs)
|
||||||
|
finally:
|
||||||
|
client.logoff()
|
||||||
|
print(json.dumps(result))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(client, inputs):
|
||||||
|
name = inputs.get("name")
|
||||||
|
if not name:
|
||||||
|
raise Exception("name is required")
|
||||||
|
address = inputs.get("address")
|
||||||
|
if not address:
|
||||||
|
raise Exception("address is required")
|
||||||
|
user_name = inputs.get("user_name")
|
||||||
|
if not user_name:
|
||||||
|
raise Exception("user_name is required")
|
||||||
|
platform_id = inputs.get("platform_id")
|
||||||
|
if not platform_id:
|
||||||
|
raise Exception("platform_id is required")
|
||||||
|
safe_name = inputs.get("safe_name")
|
||||||
|
if not safe_name:
|
||||||
|
raise Exception("safe_name is required")
|
||||||
|
secret = inputs.get("secret")
|
||||||
|
secret_type = inputs.get("secret_type")
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"name": name,
|
||||||
|
"address": address,
|
||||||
|
"userName": user_name,
|
||||||
|
"platformId": platform_id,
|
||||||
|
"safeName": safe_name,
|
||||||
|
"secretType": secret_type or "password",
|
||||||
|
}
|
||||||
|
if secret:
|
||||||
|
body["secret"] = secret
|
||||||
|
|
||||||
|
return client.call("POST", "/API/Accounts", body=body)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(cfg):
|
||||||
|
if cfg.get("insecure"):
|
||||||
|
c = ssl.create_default_context()
|
||||||
|
c.check_hostname = False
|
||||||
|
c.verify_mode = ssl.CERT_NONE
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.base = str(cfg.get("url", "")).rstrip("/") + "/PasswordVault"
|
||||||
|
self.ctx = _ctx(cfg)
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _open(self, method, path, body=None, params=None, auth=True):
|
||||||
|
url = self.base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if auth and self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def logon(self):
|
||||||
|
method = str(self.cfg.get("auth_method") or "Cyberark").strip() or "Cyberark"
|
||||||
|
body = {
|
||||||
|
"username": self.cfg.get("username", ""),
|
||||||
|
"password": self.cfg.get("password", ""),
|
||||||
|
"concurrentSession": True,
|
||||||
|
}
|
||||||
|
tok = self._open("POST", "/API/auth/" + urllib.parse.quote(method, safe="") + "/Logon", body=body, auth=False)
|
||||||
|
# tok is the session token (a plain string when parsed from the JSON body)
|
||||||
|
self.token = tok if isinstance(tok, str) else str(tok)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
try:
|
||||||
|
self._open("POST", "/API/Auth/Logoff", body={})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call(self, method, path, body=None, params=None):
|
||||||
|
return self._open(method, path, body=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
client = Client(cfg)
|
||||||
|
client.logon()
|
||||||
|
try:
|
||||||
|
result = fn(client, inputs)
|
||||||
|
finally:
|
||||||
|
client.logoff()
|
||||||
|
print(json.dumps(result))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def main(client, inputs):
|
||||||
|
account_id = inputs.get("account_id")
|
||||||
|
if not account_id:
|
||||||
|
raise Exception("account_id is required")
|
||||||
|
change_entire_group = bool(inputs.get("change_entire_group") or False)
|
||||||
|
result = client.call(
|
||||||
|
"POST",
|
||||||
|
"/API/Accounts/" + q(account_id) + "/Change",
|
||||||
|
body={"ChangeEntireGroup": change_entire_group},
|
||||||
|
)
|
||||||
|
return result if result else {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(cfg):
|
||||||
|
if cfg.get("insecure"):
|
||||||
|
c = ssl.create_default_context()
|
||||||
|
c.check_hostname = False
|
||||||
|
c.verify_mode = ssl.CERT_NONE
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.base = str(cfg.get("url", "")).rstrip("/") + "/PasswordVault"
|
||||||
|
self.ctx = _ctx(cfg)
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _open(self, method, path, body=None, params=None, auth=True):
|
||||||
|
url = self.base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if auth and self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def logon(self):
|
||||||
|
method = str(self.cfg.get("auth_method") or "Cyberark").strip() or "Cyberark"
|
||||||
|
body = {
|
||||||
|
"username": self.cfg.get("username", ""),
|
||||||
|
"password": self.cfg.get("password", ""),
|
||||||
|
"concurrentSession": True,
|
||||||
|
}
|
||||||
|
tok = self._open("POST", "/API/auth/" + urllib.parse.quote(method, safe="") + "/Logon", body=body, auth=False)
|
||||||
|
# tok is the session token (a plain string when parsed from the JSON body)
|
||||||
|
self.token = tok if isinstance(tok, str) else str(tok)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
try:
|
||||||
|
self._open("POST", "/API/Auth/Logoff", body={})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call(self, method, path, body=None, params=None):
|
||||||
|
return self._open(method, path, body=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
client = Client(cfg)
|
||||||
|
client.logon()
|
||||||
|
try:
|
||||||
|
result = fn(client, inputs)
|
||||||
|
finally:
|
||||||
|
client.logoff()
|
||||||
|
print(json.dumps(result))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def main(client, inputs):
|
||||||
|
account_id = inputs.get("account_id")
|
||||||
|
if not account_id:
|
||||||
|
raise Exception("account_id is required")
|
||||||
|
client.call("DELETE", "/API/Accounts/" + q(account_id))
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(cfg):
|
||||||
|
if cfg.get("insecure"):
|
||||||
|
c = ssl.create_default_context()
|
||||||
|
c.check_hostname = False
|
||||||
|
c.verify_mode = ssl.CERT_NONE
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.base = str(cfg.get("url", "")).rstrip("/") + "/PasswordVault"
|
||||||
|
self.ctx = _ctx(cfg)
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _open(self, method, path, body=None, params=None, auth=True):
|
||||||
|
url = self.base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if auth and self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def logon(self):
|
||||||
|
method = str(self.cfg.get("auth_method") or "Cyberark").strip() or "Cyberark"
|
||||||
|
body = {
|
||||||
|
"username": self.cfg.get("username", ""),
|
||||||
|
"password": self.cfg.get("password", ""),
|
||||||
|
"concurrentSession": True,
|
||||||
|
}
|
||||||
|
tok = self._open("POST", "/API/auth/" + urllib.parse.quote(method, safe="") + "/Logon", body=body, auth=False)
|
||||||
|
# tok is the session token (a plain string when parsed from the JSON body)
|
||||||
|
self.token = tok if isinstance(tok, str) else str(tok)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
try:
|
||||||
|
self._open("POST", "/API/Auth/Logoff", body={})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call(self, method, path, body=None, params=None):
|
||||||
|
return self._open(method, path, body=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
client = Client(cfg)
|
||||||
|
client.logon()
|
||||||
|
try:
|
||||||
|
result = fn(client, inputs)
|
||||||
|
finally:
|
||||||
|
client.logoff()
|
||||||
|
print(json.dumps(result))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def main(client, inputs):
|
||||||
|
account_id = inputs.get("account_id")
|
||||||
|
if not account_id:
|
||||||
|
raise Exception("account_id is required")
|
||||||
|
return client.call("GET", "/API/Accounts/" + q(account_id))
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(cfg):
|
||||||
|
if cfg.get("insecure"):
|
||||||
|
c = ssl.create_default_context()
|
||||||
|
c.check_hostname = False
|
||||||
|
c.verify_mode = ssl.CERT_NONE
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.base = str(cfg.get("url", "")).rstrip("/") + "/PasswordVault"
|
||||||
|
self.ctx = _ctx(cfg)
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _open(self, method, path, body=None, params=None, auth=True):
|
||||||
|
url = self.base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if auth and self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def logon(self):
|
||||||
|
method = str(self.cfg.get("auth_method") or "Cyberark").strip() or "Cyberark"
|
||||||
|
body = {
|
||||||
|
"username": self.cfg.get("username", ""),
|
||||||
|
"password": self.cfg.get("password", ""),
|
||||||
|
"concurrentSession": True,
|
||||||
|
}
|
||||||
|
tok = self._open("POST", "/API/auth/" + urllib.parse.quote(method, safe="") + "/Logon", body=body, auth=False)
|
||||||
|
# tok is the session token (a plain string when parsed from the JSON body)
|
||||||
|
self.token = tok if isinstance(tok, str) else str(tok)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
try:
|
||||||
|
self._open("POST", "/API/Auth/Logoff", body={})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call(self, method, path, body=None, params=None):
|
||||||
|
return self._open(method, path, body=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
client = Client(cfg)
|
||||||
|
client.logon()
|
||||||
|
try:
|
||||||
|
result = fn(client, inputs)
|
||||||
|
finally:
|
||||||
|
client.logoff()
|
||||||
|
print(json.dumps(result))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def main(client, inputs):
|
||||||
|
account_id = inputs.get("account_id")
|
||||||
|
if not account_id:
|
||||||
|
raise Exception("account_id is required")
|
||||||
|
reason = inputs.get("reason")
|
||||||
|
body = {"reason": reason} if reason else {}
|
||||||
|
result = client.call("POST", "/API/Accounts/" + q(account_id) + "/Password/Retrieve", body=body)
|
||||||
|
return {"password": result}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(cfg):
|
||||||
|
if cfg.get("insecure"):
|
||||||
|
c = ssl.create_default_context()
|
||||||
|
c.check_hostname = False
|
||||||
|
c.verify_mode = ssl.CERT_NONE
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.base = str(cfg.get("url", "")).rstrip("/") + "/PasswordVault"
|
||||||
|
self.ctx = _ctx(cfg)
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _open(self, method, path, body=None, params=None, auth=True):
|
||||||
|
url = self.base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if auth and self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def logon(self):
|
||||||
|
method = str(self.cfg.get("auth_method") or "Cyberark").strip() or "Cyberark"
|
||||||
|
body = {
|
||||||
|
"username": self.cfg.get("username", ""),
|
||||||
|
"password": self.cfg.get("password", ""),
|
||||||
|
"concurrentSession": True,
|
||||||
|
}
|
||||||
|
tok = self._open("POST", "/API/auth/" + urllib.parse.quote(method, safe="") + "/Logon", body=body, auth=False)
|
||||||
|
# tok is the session token (a plain string when parsed from the JSON body)
|
||||||
|
self.token = tok if isinstance(tok, str) else str(tok)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
try:
|
||||||
|
self._open("POST", "/API/Auth/Logoff", body={})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call(self, method, path, body=None, params=None):
|
||||||
|
return self._open(method, path, body=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
client = Client(cfg)
|
||||||
|
client.logon()
|
||||||
|
try:
|
||||||
|
result = fn(client, inputs)
|
||||||
|
finally:
|
||||||
|
client.logoff()
|
||||||
|
print(json.dumps(result))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(client, inputs):
|
||||||
|
search = inputs.get("search")
|
||||||
|
filter_ = inputs.get("filter")
|
||||||
|
limit = int(inputs.get("limit") or 50)
|
||||||
|
return client.call("GET", "/API/Accounts", params={"search": search, "filter": filter_, "limit": limit})
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(cfg):
|
||||||
|
if cfg.get("insecure"):
|
||||||
|
c = ssl.create_default_context()
|
||||||
|
c.check_hostname = False
|
||||||
|
c.verify_mode = ssl.CERT_NONE
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.base = str(cfg.get("url", "")).rstrip("/") + "/PasswordVault"
|
||||||
|
self.ctx = _ctx(cfg)
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _open(self, method, path, body=None, params=None, auth=True):
|
||||||
|
url = self.base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if auth and self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def logon(self):
|
||||||
|
method = str(self.cfg.get("auth_method") or "Cyberark").strip() or "Cyberark"
|
||||||
|
body = {
|
||||||
|
"username": self.cfg.get("username", ""),
|
||||||
|
"password": self.cfg.get("password", ""),
|
||||||
|
"concurrentSession": True,
|
||||||
|
}
|
||||||
|
tok = self._open("POST", "/API/auth/" + urllib.parse.quote(method, safe="") + "/Logon", body=body, auth=False)
|
||||||
|
# tok is the session token (a plain string when parsed from the JSON body)
|
||||||
|
self.token = tok if isinstance(tok, str) else str(tok)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
try:
|
||||||
|
self._open("POST", "/API/Auth/Logoff", body={})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call(self, method, path, body=None, params=None):
|
||||||
|
return self._open(method, path, body=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
client = Client(cfg)
|
||||||
|
client.logon()
|
||||||
|
try:
|
||||||
|
result = fn(client, inputs)
|
||||||
|
finally:
|
||||||
|
client.logoff()
|
||||||
|
print(json.dumps(result))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(client, inputs):
|
||||||
|
search = inputs.get("search")
|
||||||
|
limit = int(inputs.get("limit") or 50)
|
||||||
|
return client.call("GET", "/API/Safes", params={"search": search, "limit": limit})
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(cfg):
|
||||||
|
if cfg.get("insecure"):
|
||||||
|
c = ssl.create_default_context()
|
||||||
|
c.check_hostname = False
|
||||||
|
c.verify_mode = ssl.CERT_NONE
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.base = str(cfg.get("url", "")).rstrip("/") + "/PasswordVault"
|
||||||
|
self.ctx = _ctx(cfg)
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _open(self, method, path, body=None, params=None, auth=True):
|
||||||
|
url = self.base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if auth and self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def logon(self):
|
||||||
|
method = str(self.cfg.get("auth_method") or "Cyberark").strip() or "Cyberark"
|
||||||
|
body = {
|
||||||
|
"username": self.cfg.get("username", ""),
|
||||||
|
"password": self.cfg.get("password", ""),
|
||||||
|
"concurrentSession": True,
|
||||||
|
}
|
||||||
|
tok = self._open("POST", "/API/auth/" + urllib.parse.quote(method, safe="") + "/Logon", body=body, auth=False)
|
||||||
|
# tok is the session token (a plain string when parsed from the JSON body)
|
||||||
|
self.token = tok if isinstance(tok, str) else str(tok)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
try:
|
||||||
|
self._open("POST", "/API/Auth/Logoff", body={})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call(self, method, path, body=None, params=None):
|
||||||
|
return self._open(method, path, body=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
client = Client(cfg)
|
||||||
|
client.logon()
|
||||||
|
try:
|
||||||
|
result = fn(client, inputs)
|
||||||
|
finally:
|
||||||
|
client.logoff()
|
||||||
|
print(json.dumps(result))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(client, inputs):
|
||||||
|
search = inputs.get("search")
|
||||||
|
filter_ = inputs.get("filter")
|
||||||
|
return client.call("GET", "/API/Users", params={"search": search, "filter": filter_})
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(cfg):
|
||||||
|
if cfg.get("insecure"):
|
||||||
|
c = ssl.create_default_context()
|
||||||
|
c.check_hostname = False
|
||||||
|
c.verify_mode = ssl.CERT_NONE
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.base = str(cfg.get("url", "")).rstrip("/") + "/PasswordVault"
|
||||||
|
self.ctx = _ctx(cfg)
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _open(self, method, path, body=None, params=None, auth=True):
|
||||||
|
url = self.base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if auth and self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def logon(self):
|
||||||
|
method = str(self.cfg.get("auth_method") or "Cyberark").strip() or "Cyberark"
|
||||||
|
body = {
|
||||||
|
"username": self.cfg.get("username", ""),
|
||||||
|
"password": self.cfg.get("password", ""),
|
||||||
|
"concurrentSession": True,
|
||||||
|
}
|
||||||
|
tok = self._open("POST", "/API/auth/" + urllib.parse.quote(method, safe="") + "/Logon", body=body, auth=False)
|
||||||
|
# tok is the session token (a plain string when parsed from the JSON body)
|
||||||
|
self.token = tok if isinstance(tok, str) else str(tok)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
try:
|
||||||
|
self._open("POST", "/API/Auth/Logoff", body={})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call(self, method, path, body=None, params=None):
|
||||||
|
return self._open(method, path, body=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
client = Client(cfg)
|
||||||
|
client.logon()
|
||||||
|
try:
|
||||||
|
result = fn(client, inputs)
|
||||||
|
finally:
|
||||||
|
client.logoff()
|
||||||
|
print(json.dumps(result))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def main(client, inputs):
|
||||||
|
account_id = inputs.get("account_id")
|
||||||
|
if not account_id:
|
||||||
|
raise Exception("account_id is required")
|
||||||
|
result = client.call("POST", "/API/Accounts/" + q(account_id) + "/Reconcile", body={})
|
||||||
|
return {"ok": True} if not result else result
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(cfg):
|
||||||
|
if cfg.get("insecure"):
|
||||||
|
c = ssl.create_default_context()
|
||||||
|
c.check_hostname = False
|
||||||
|
c.verify_mode = ssl.CERT_NONE
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.base = str(cfg.get("url", "")).rstrip("/") + "/PasswordVault"
|
||||||
|
self.ctx = _ctx(cfg)
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _open(self, method, path, body=None, params=None, auth=True):
|
||||||
|
url = self.base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if auth and self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def logon(self):
|
||||||
|
method = str(self.cfg.get("auth_method") or "Cyberark").strip() or "Cyberark"
|
||||||
|
body = {
|
||||||
|
"username": self.cfg.get("username", ""),
|
||||||
|
"password": self.cfg.get("password", ""),
|
||||||
|
"concurrentSession": True,
|
||||||
|
}
|
||||||
|
tok = self._open("POST", "/API/auth/" + urllib.parse.quote(method, safe="") + "/Logon", body=body, auth=False)
|
||||||
|
# tok is the session token (a plain string when parsed from the JSON body)
|
||||||
|
self.token = tok if isinstance(tok, str) else str(tok)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
try:
|
||||||
|
self._open("POST", "/API/Auth/Logoff", body={})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call(self, method, path, body=None, params=None):
|
||||||
|
return self._open(method, path, body=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
client = Client(cfg)
|
||||||
|
client.logon()
|
||||||
|
try:
|
||||||
|
result = fn(client, inputs)
|
||||||
|
finally:
|
||||||
|
client.logoff()
|
||||||
|
print(json.dumps(result))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(client, inputs):
|
||||||
|
client.call("GET", "/API/Safes", params={"limit": 1})
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx(cfg):
|
||||||
|
if cfg.get("insecure"):
|
||||||
|
c = ssl.create_default_context()
|
||||||
|
c.check_hostname = False
|
||||||
|
c.verify_mode = ssl.CERT_NONE
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.cfg = cfg
|
||||||
|
self.base = str(cfg.get("url", "")).rstrip("/") + "/PasswordVault"
|
||||||
|
self.ctx = _ctx(cfg)
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _open(self, method, path, body=None, params=None, auth=True):
|
||||||
|
url = self.base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if auth and self.token:
|
||||||
|
headers["Authorization"] = self.token
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def logon(self):
|
||||||
|
method = str(self.cfg.get("auth_method") or "Cyberark").strip() or "Cyberark"
|
||||||
|
body = {
|
||||||
|
"username": self.cfg.get("username", ""),
|
||||||
|
"password": self.cfg.get("password", ""),
|
||||||
|
"concurrentSession": True,
|
||||||
|
}
|
||||||
|
tok = self._open("POST", "/API/auth/" + urllib.parse.quote(method, safe="") + "/Logon", body=body, auth=False)
|
||||||
|
# tok is the session token (a plain string when parsed from the JSON body)
|
||||||
|
self.token = tok if isinstance(tok, str) else str(tok)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def logoff(self):
|
||||||
|
try:
|
||||||
|
self._open("POST", "/API/Auth/Logoff", body={})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call(self, method, path, body=None, params=None):
|
||||||
|
return self._open(method, path, body=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
client = Client(cfg)
|
||||||
|
client.logon()
|
||||||
|
try:
|
||||||
|
result = fn(client, inputs)
|
||||||
|
finally:
|
||||||
|
client.logoff()
|
||||||
|
print(json.dumps(result))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def main(client, inputs):
|
||||||
|
account_id = inputs.get("account_id")
|
||||||
|
if not account_id:
|
||||||
|
raise Exception("account_id is required")
|
||||||
|
result = client.call("POST", "/API/Accounts/" + q(account_id) + "/Verify", body={})
|
||||||
|
return {"ok": True} if not result else result
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
id: duo
|
||||||
|
name: Cisco Duo
|
||||||
|
version: 1.0.0
|
||||||
|
description: "Cisco Duo (Admin API) — MFA/identity containment and investigation: list and read users, disable/enable/bypass a user (containment), delete users, read bypass codes and devices, and pull authentication logs. HMAC-SHA1 signed authentication; stdlib-only, no extra Python dependencies."
|
||||||
|
changelog: "1.0.0 — Initial release: get users/user, modify user status (disable/enable/bypass), delete user, get bypass codes, get devices, get user devices, authentication logs."
|
||||||
|
category: identity
|
||||||
|
|
||||||
|
# Per-instance configuration. Requests are signed (HMAC-SHA1) with the
|
||||||
|
# integration key + secret key against the Admin API hostname.
|
||||||
|
config_schema:
|
||||||
|
properties:
|
||||||
|
api_hostname:
|
||||||
|
type: string
|
||||||
|
description: "Duo Admin API hostname (e.g. api-xxxxxxxx.duosecurity.com)"
|
||||||
|
integration_key:
|
||||||
|
type: string
|
||||||
|
description: "Admin API integration key (ikey)"
|
||||||
|
x-soar-sensitive: true
|
||||||
|
secret_key:
|
||||||
|
type: string
|
||||||
|
description: "Admin API secret key (skey)"
|
||||||
|
x-soar-sensitive: true
|
||||||
|
required:
|
||||||
|
- api_hostname
|
||||||
|
- integration_key
|
||||||
|
- secret_key
|
||||||
|
|
||||||
|
commands:
|
||||||
|
- id: get_users
|
||||||
|
name: duo-get-users
|
||||||
|
description: "List users (optionally filter by exact username)."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
username: { type: string, description: "Optional exact username to look up" }
|
||||||
|
limit: { type: number, description: "Max users (default 100)" }
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: get_user
|
||||||
|
name: duo-get-user
|
||||||
|
description: "Get a single user by user ID."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "Duo user ID" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: modify_user
|
||||||
|
name: duo-modify-user
|
||||||
|
description: "Change a user's status — disable (containment), enable (active), or bypass."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "Duo user ID" }
|
||||||
|
status: { type: string, description: "active | disabled | bypass" }
|
||||||
|
required: [user_id, status]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: delete_user
|
||||||
|
name: duo-delete-user
|
||||||
|
description: "Delete a user."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "Duo user ID" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: get_bypass_codes
|
||||||
|
name: duo-get-bypass-codes
|
||||||
|
description: "List a user's bypass codes."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "Duo user ID" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: get_devices
|
||||||
|
name: duo-get-devices
|
||||||
|
description: "List all phones/devices in the account."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
limit: { type: number, description: "Max devices (default 100)" }
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: get_user_devices
|
||||||
|
name: duo-get-user-devices
|
||||||
|
description: "List the phones/devices associated with a user."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "Duo user ID" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: get_authentication_logs
|
||||||
|
name: duo-get-authentication-logs
|
||||||
|
description: "Pull authentication logs for a time window."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
mintime: { type: number, description: "Start time as Unix epoch milliseconds (required by Duo v2 logs)" }
|
||||||
|
maxtime: { type: number, description: "End time as Unix epoch milliseconds" }
|
||||||
|
limit: { type: number, description: "Max events (default 100, max 1000)" }
|
||||||
|
required: [mintime, maxtime]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
|
||||||
|
- id: test_connection
|
||||||
|
name: duo-test-connection
|
||||||
|
description: "Verify connectivity and the signed credentials (used by the Test button)."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import json, os, sys, hmac, hashlib, base64, email.utils
|
||||||
|
import urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _canon_params(params):
|
||||||
|
# RFC-3986 encode each key/value, sort by key, join k=v with &
|
||||||
|
items = []
|
||||||
|
for k in sorted(params.keys()):
|
||||||
|
ek = urllib.parse.quote(str(k), "~")
|
||||||
|
ev = urllib.parse.quote(str(params[k]), "~")
|
||||||
|
items.append(ek + "=" + ev)
|
||||||
|
return "&".join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(method, host, path, params, cfg, now):
|
||||||
|
canon = "\n".join([now, method.upper(), host.lower(), path, _canon_params(params)])
|
||||||
|
skey = str(cfg.get("secret_key", "")).encode("utf-8")
|
||||||
|
sig = hmac.new(skey, canon.encode("utf-8"), hashlib.sha1).hexdigest()
|
||||||
|
ikey = str(cfg.get("integration_key", ""))
|
||||||
|
auth = base64.b64encode((ikey + ":" + sig).encode("utf-8")).decode("utf-8")
|
||||||
|
return "Basic " + auth
|
||||||
|
|
||||||
|
|
||||||
|
def call(method, path, cfg, params=None):
|
||||||
|
params = params or {}
|
||||||
|
host = str(cfg.get("api_hostname", "")).strip()
|
||||||
|
now = email.utils.formatdate() # RFC 2822, e.g. 'Wed, 01 Jan 2020 00:00:00 -0000'
|
||||||
|
authz = _sign(method, host, path, params, cfg, now)
|
||||||
|
headers = {"Authorization": authz, "Date": now, "Accept": "application/json"}
|
||||||
|
method = method.upper()
|
||||||
|
url = "https://" + host + path
|
||||||
|
data = None
|
||||||
|
if method in ("GET", "DELETE"):
|
||||||
|
if params:
|
||||||
|
url += "?" + _canon_params(params)
|
||||||
|
else:
|
||||||
|
# POST: params go in the body, form-encoded with the SAME canonicalization
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
data = _canon_params(params).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = str(inputs.get("user_id", "") or "").strip()
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
path = "/admin/v1/users/" + q(user_id)
|
||||||
|
call("DELETE", path, cfg)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import json, os, sys, hmac, hashlib, base64, email.utils
|
||||||
|
import urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _canon_params(params):
|
||||||
|
# RFC-3986 encode each key/value, sort by key, join k=v with &
|
||||||
|
items = []
|
||||||
|
for k in sorted(params.keys()):
|
||||||
|
ek = urllib.parse.quote(str(k), "~")
|
||||||
|
ev = urllib.parse.quote(str(params[k]), "~")
|
||||||
|
items.append(ek + "=" + ev)
|
||||||
|
return "&".join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(method, host, path, params, cfg, now):
|
||||||
|
canon = "\n".join([now, method.upper(), host.lower(), path, _canon_params(params)])
|
||||||
|
skey = str(cfg.get("secret_key", "")).encode("utf-8")
|
||||||
|
sig = hmac.new(skey, canon.encode("utf-8"), hashlib.sha1).hexdigest()
|
||||||
|
ikey = str(cfg.get("integration_key", ""))
|
||||||
|
auth = base64.b64encode((ikey + ":" + sig).encode("utf-8")).decode("utf-8")
|
||||||
|
return "Basic " + auth
|
||||||
|
|
||||||
|
|
||||||
|
def call(method, path, cfg, params=None):
|
||||||
|
params = params or {}
|
||||||
|
host = str(cfg.get("api_hostname", "")).strip()
|
||||||
|
now = email.utils.formatdate() # RFC 2822, e.g. 'Wed, 01 Jan 2020 00:00:00 -0000'
|
||||||
|
authz = _sign(method, host, path, params, cfg, now)
|
||||||
|
headers = {"Authorization": authz, "Date": now, "Accept": "application/json"}
|
||||||
|
method = method.upper()
|
||||||
|
url = "https://" + host + path
|
||||||
|
data = None
|
||||||
|
if method in ("GET", "DELETE"):
|
||||||
|
if params:
|
||||||
|
url += "?" + _canon_params(params)
|
||||||
|
else:
|
||||||
|
# POST: params go in the body, form-encoded with the SAME canonicalization
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
data = _canon_params(params).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
mintime = inputs.get("mintime", "")
|
||||||
|
maxtime = inputs.get("maxtime", "")
|
||||||
|
if mintime == "" or mintime is None:
|
||||||
|
raise Exception("mintime is required")
|
||||||
|
if maxtime == "" or maxtime is None:
|
||||||
|
raise Exception("maxtime is required")
|
||||||
|
limit = inputs.get("limit", 100)
|
||||||
|
params = {"mintime": int(mintime), "maxtime": int(maxtime), "limit": int(limit)}
|
||||||
|
return call("GET", "/admin/v2/logs/authentication", cfg, params)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import json, os, sys, hmac, hashlib, base64, email.utils
|
||||||
|
import urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _canon_params(params):
|
||||||
|
# RFC-3986 encode each key/value, sort by key, join k=v with &
|
||||||
|
items = []
|
||||||
|
for k in sorted(params.keys()):
|
||||||
|
ek = urllib.parse.quote(str(k), "~")
|
||||||
|
ev = urllib.parse.quote(str(params[k]), "~")
|
||||||
|
items.append(ek + "=" + ev)
|
||||||
|
return "&".join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(method, host, path, params, cfg, now):
|
||||||
|
canon = "\n".join([now, method.upper(), host.lower(), path, _canon_params(params)])
|
||||||
|
skey = str(cfg.get("secret_key", "")).encode("utf-8")
|
||||||
|
sig = hmac.new(skey, canon.encode("utf-8"), hashlib.sha1).hexdigest()
|
||||||
|
ikey = str(cfg.get("integration_key", ""))
|
||||||
|
auth = base64.b64encode((ikey + ":" + sig).encode("utf-8")).decode("utf-8")
|
||||||
|
return "Basic " + auth
|
||||||
|
|
||||||
|
|
||||||
|
def call(method, path, cfg, params=None):
|
||||||
|
params = params or {}
|
||||||
|
host = str(cfg.get("api_hostname", "")).strip()
|
||||||
|
now = email.utils.formatdate() # RFC 2822, e.g. 'Wed, 01 Jan 2020 00:00:00 -0000'
|
||||||
|
authz = _sign(method, host, path, params, cfg, now)
|
||||||
|
headers = {"Authorization": authz, "Date": now, "Accept": "application/json"}
|
||||||
|
method = method.upper()
|
||||||
|
url = "https://" + host + path
|
||||||
|
data = None
|
||||||
|
if method in ("GET", "DELETE"):
|
||||||
|
if params:
|
||||||
|
url += "?" + _canon_params(params)
|
||||||
|
else:
|
||||||
|
# POST: params go in the body, form-encoded with the SAME canonicalization
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
data = _canon_params(params).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = str(inputs.get("user_id", "") or "").strip()
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
path = "/admin/v1/users/" + q(user_id) + "/bypass_codes"
|
||||||
|
return call("GET", path, cfg)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import json, os, sys, hmac, hashlib, base64, email.utils
|
||||||
|
import urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _canon_params(params):
|
||||||
|
# RFC-3986 encode each key/value, sort by key, join k=v with &
|
||||||
|
items = []
|
||||||
|
for k in sorted(params.keys()):
|
||||||
|
ek = urllib.parse.quote(str(k), "~")
|
||||||
|
ev = urllib.parse.quote(str(params[k]), "~")
|
||||||
|
items.append(ek + "=" + ev)
|
||||||
|
return "&".join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(method, host, path, params, cfg, now):
|
||||||
|
canon = "\n".join([now, method.upper(), host.lower(), path, _canon_params(params)])
|
||||||
|
skey = str(cfg.get("secret_key", "")).encode("utf-8")
|
||||||
|
sig = hmac.new(skey, canon.encode("utf-8"), hashlib.sha1).hexdigest()
|
||||||
|
ikey = str(cfg.get("integration_key", ""))
|
||||||
|
auth = base64.b64encode((ikey + ":" + sig).encode("utf-8")).decode("utf-8")
|
||||||
|
return "Basic " + auth
|
||||||
|
|
||||||
|
|
||||||
|
def call(method, path, cfg, params=None):
|
||||||
|
params = params or {}
|
||||||
|
host = str(cfg.get("api_hostname", "")).strip()
|
||||||
|
now = email.utils.formatdate() # RFC 2822, e.g. 'Wed, 01 Jan 2020 00:00:00 -0000'
|
||||||
|
authz = _sign(method, host, path, params, cfg, now)
|
||||||
|
headers = {"Authorization": authz, "Date": now, "Accept": "application/json"}
|
||||||
|
method = method.upper()
|
||||||
|
url = "https://" + host + path
|
||||||
|
data = None
|
||||||
|
if method in ("GET", "DELETE"):
|
||||||
|
if params:
|
||||||
|
url += "?" + _canon_params(params)
|
||||||
|
else:
|
||||||
|
# POST: params go in the body, form-encoded with the SAME canonicalization
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
data = _canon_params(params).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
limit = inputs.get("limit", 100)
|
||||||
|
return call("GET", "/admin/v1/phones", cfg, {"limit": int(limit)})
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import json, os, sys, hmac, hashlib, base64, email.utils
|
||||||
|
import urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _canon_params(params):
|
||||||
|
# RFC-3986 encode each key/value, sort by key, join k=v with &
|
||||||
|
items = []
|
||||||
|
for k in sorted(params.keys()):
|
||||||
|
ek = urllib.parse.quote(str(k), "~")
|
||||||
|
ev = urllib.parse.quote(str(params[k]), "~")
|
||||||
|
items.append(ek + "=" + ev)
|
||||||
|
return "&".join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(method, host, path, params, cfg, now):
|
||||||
|
canon = "\n".join([now, method.upper(), host.lower(), path, _canon_params(params)])
|
||||||
|
skey = str(cfg.get("secret_key", "")).encode("utf-8")
|
||||||
|
sig = hmac.new(skey, canon.encode("utf-8"), hashlib.sha1).hexdigest()
|
||||||
|
ikey = str(cfg.get("integration_key", ""))
|
||||||
|
auth = base64.b64encode((ikey + ":" + sig).encode("utf-8")).decode("utf-8")
|
||||||
|
return "Basic " + auth
|
||||||
|
|
||||||
|
|
||||||
|
def call(method, path, cfg, params=None):
|
||||||
|
params = params or {}
|
||||||
|
host = str(cfg.get("api_hostname", "")).strip()
|
||||||
|
now = email.utils.formatdate() # RFC 2822, e.g. 'Wed, 01 Jan 2020 00:00:00 -0000'
|
||||||
|
authz = _sign(method, host, path, params, cfg, now)
|
||||||
|
headers = {"Authorization": authz, "Date": now, "Accept": "application/json"}
|
||||||
|
method = method.upper()
|
||||||
|
url = "https://" + host + path
|
||||||
|
data = None
|
||||||
|
if method in ("GET", "DELETE"):
|
||||||
|
if params:
|
||||||
|
url += "?" + _canon_params(params)
|
||||||
|
else:
|
||||||
|
# POST: params go in the body, form-encoded with the SAME canonicalization
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
data = _canon_params(params).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = str(inputs.get("user_id", "") or "").strip()
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
path = "/admin/v1/users/" + q(user_id)
|
||||||
|
return call("GET", path, cfg)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import json, os, sys, hmac, hashlib, base64, email.utils
|
||||||
|
import urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _canon_params(params):
|
||||||
|
# RFC-3986 encode each key/value, sort by key, join k=v with &
|
||||||
|
items = []
|
||||||
|
for k in sorted(params.keys()):
|
||||||
|
ek = urllib.parse.quote(str(k), "~")
|
||||||
|
ev = urllib.parse.quote(str(params[k]), "~")
|
||||||
|
items.append(ek + "=" + ev)
|
||||||
|
return "&".join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(method, host, path, params, cfg, now):
|
||||||
|
canon = "\n".join([now, method.upper(), host.lower(), path, _canon_params(params)])
|
||||||
|
skey = str(cfg.get("secret_key", "")).encode("utf-8")
|
||||||
|
sig = hmac.new(skey, canon.encode("utf-8"), hashlib.sha1).hexdigest()
|
||||||
|
ikey = str(cfg.get("integration_key", ""))
|
||||||
|
auth = base64.b64encode((ikey + ":" + sig).encode("utf-8")).decode("utf-8")
|
||||||
|
return "Basic " + auth
|
||||||
|
|
||||||
|
|
||||||
|
def call(method, path, cfg, params=None):
|
||||||
|
params = params or {}
|
||||||
|
host = str(cfg.get("api_hostname", "")).strip()
|
||||||
|
now = email.utils.formatdate() # RFC 2822, e.g. 'Wed, 01 Jan 2020 00:00:00 -0000'
|
||||||
|
authz = _sign(method, host, path, params, cfg, now)
|
||||||
|
headers = {"Authorization": authz, "Date": now, "Accept": "application/json"}
|
||||||
|
method = method.upper()
|
||||||
|
url = "https://" + host + path
|
||||||
|
data = None
|
||||||
|
if method in ("GET", "DELETE"):
|
||||||
|
if params:
|
||||||
|
url += "?" + _canon_params(params)
|
||||||
|
else:
|
||||||
|
# POST: params go in the body, form-encoded with the SAME canonicalization
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
data = _canon_params(params).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = str(inputs.get("user_id", "") or "").strip()
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
path = "/admin/v1/users/" + q(user_id) + "/phones"
|
||||||
|
return call("GET", path, cfg)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import json, os, sys, hmac, hashlib, base64, email.utils
|
||||||
|
import urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _canon_params(params):
|
||||||
|
# RFC-3986 encode each key/value, sort by key, join k=v with &
|
||||||
|
items = []
|
||||||
|
for k in sorted(params.keys()):
|
||||||
|
ek = urllib.parse.quote(str(k), "~")
|
||||||
|
ev = urllib.parse.quote(str(params[k]), "~")
|
||||||
|
items.append(ek + "=" + ev)
|
||||||
|
return "&".join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(method, host, path, params, cfg, now):
|
||||||
|
canon = "\n".join([now, method.upper(), host.lower(), path, _canon_params(params)])
|
||||||
|
skey = str(cfg.get("secret_key", "")).encode("utf-8")
|
||||||
|
sig = hmac.new(skey, canon.encode("utf-8"), hashlib.sha1).hexdigest()
|
||||||
|
ikey = str(cfg.get("integration_key", ""))
|
||||||
|
auth = base64.b64encode((ikey + ":" + sig).encode("utf-8")).decode("utf-8")
|
||||||
|
return "Basic " + auth
|
||||||
|
|
||||||
|
|
||||||
|
def call(method, path, cfg, params=None):
|
||||||
|
params = params or {}
|
||||||
|
host = str(cfg.get("api_hostname", "")).strip()
|
||||||
|
now = email.utils.formatdate() # RFC 2822, e.g. 'Wed, 01 Jan 2020 00:00:00 -0000'
|
||||||
|
authz = _sign(method, host, path, params, cfg, now)
|
||||||
|
headers = {"Authorization": authz, "Date": now, "Accept": "application/json"}
|
||||||
|
method = method.upper()
|
||||||
|
url = "https://" + host + path
|
||||||
|
data = None
|
||||||
|
if method in ("GET", "DELETE"):
|
||||||
|
if params:
|
||||||
|
url += "?" + _canon_params(params)
|
||||||
|
else:
|
||||||
|
# POST: params go in the body, form-encoded with the SAME canonicalization
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
data = _canon_params(params).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
username = str(inputs.get("username", "") or "").strip()
|
||||||
|
if username:
|
||||||
|
params = {"username": username}
|
||||||
|
else:
|
||||||
|
limit = inputs.get("limit", 100)
|
||||||
|
params = {"limit": int(limit)}
|
||||||
|
return call("GET", "/admin/v1/users", cfg, params)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import json, os, sys, hmac, hashlib, base64, email.utils
|
||||||
|
import urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _canon_params(params):
|
||||||
|
# RFC-3986 encode each key/value, sort by key, join k=v with &
|
||||||
|
items = []
|
||||||
|
for k in sorted(params.keys()):
|
||||||
|
ek = urllib.parse.quote(str(k), "~")
|
||||||
|
ev = urllib.parse.quote(str(params[k]), "~")
|
||||||
|
items.append(ek + "=" + ev)
|
||||||
|
return "&".join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(method, host, path, params, cfg, now):
|
||||||
|
canon = "\n".join([now, method.upper(), host.lower(), path, _canon_params(params)])
|
||||||
|
skey = str(cfg.get("secret_key", "")).encode("utf-8")
|
||||||
|
sig = hmac.new(skey, canon.encode("utf-8"), hashlib.sha1).hexdigest()
|
||||||
|
ikey = str(cfg.get("integration_key", ""))
|
||||||
|
auth = base64.b64encode((ikey + ":" + sig).encode("utf-8")).decode("utf-8")
|
||||||
|
return "Basic " + auth
|
||||||
|
|
||||||
|
|
||||||
|
def call(method, path, cfg, params=None):
|
||||||
|
params = params or {}
|
||||||
|
host = str(cfg.get("api_hostname", "")).strip()
|
||||||
|
now = email.utils.formatdate() # RFC 2822, e.g. 'Wed, 01 Jan 2020 00:00:00 -0000'
|
||||||
|
authz = _sign(method, host, path, params, cfg, now)
|
||||||
|
headers = {"Authorization": authz, "Date": now, "Accept": "application/json"}
|
||||||
|
method = method.upper()
|
||||||
|
url = "https://" + host + path
|
||||||
|
data = None
|
||||||
|
if method in ("GET", "DELETE"):
|
||||||
|
if params:
|
||||||
|
url += "?" + _canon_params(params)
|
||||||
|
else:
|
||||||
|
# POST: params go in the body, form-encoded with the SAME canonicalization
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
data = _canon_params(params).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = str(inputs.get("user_id", "") or "").strip()
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
status = str(inputs.get("status", "") or "").strip()
|
||||||
|
if not status:
|
||||||
|
raise Exception("status is required")
|
||||||
|
if status not in ("active", "disabled", "bypass"):
|
||||||
|
raise Exception("status must be active, disabled, or bypass")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
path = "/admin/v1/users/" + q(user_id)
|
||||||
|
return call("POST", path, cfg, {"status": status})
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import json, os, sys, hmac, hashlib, base64, email.utils
|
||||||
|
import urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _canon_params(params):
|
||||||
|
# RFC-3986 encode each key/value, sort by key, join k=v with &
|
||||||
|
items = []
|
||||||
|
for k in sorted(params.keys()):
|
||||||
|
ek = urllib.parse.quote(str(k), "~")
|
||||||
|
ev = urllib.parse.quote(str(params[k]), "~")
|
||||||
|
items.append(ek + "=" + ev)
|
||||||
|
return "&".join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(method, host, path, params, cfg, now):
|
||||||
|
canon = "\n".join([now, method.upper(), host.lower(), path, _canon_params(params)])
|
||||||
|
skey = str(cfg.get("secret_key", "")).encode("utf-8")
|
||||||
|
sig = hmac.new(skey, canon.encode("utf-8"), hashlib.sha1).hexdigest()
|
||||||
|
ikey = str(cfg.get("integration_key", ""))
|
||||||
|
auth = base64.b64encode((ikey + ":" + sig).encode("utf-8")).decode("utf-8")
|
||||||
|
return "Basic " + auth
|
||||||
|
|
||||||
|
|
||||||
|
def call(method, path, cfg, params=None):
|
||||||
|
params = params or {}
|
||||||
|
host = str(cfg.get("api_hostname", "")).strip()
|
||||||
|
now = email.utils.formatdate() # RFC 2822, e.g. 'Wed, 01 Jan 2020 00:00:00 -0000'
|
||||||
|
authz = _sign(method, host, path, params, cfg, now)
|
||||||
|
headers = {"Authorization": authz, "Date": now, "Accept": "application/json"}
|
||||||
|
method = method.upper()
|
||||||
|
url = "https://" + host + path
|
||||||
|
data = None
|
||||||
|
if method in ("GET", "DELETE"):
|
||||||
|
if params:
|
||||||
|
url += "?" + _canon_params(params)
|
||||||
|
else:
|
||||||
|
# POST: params go in the body, form-encoded with the SAME canonicalization
|
||||||
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
data = _canon_params(params).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
call("GET", "/admin/v1/users", cfg, {"limit": 1})
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
id: okta
|
||||||
|
name: Okta
|
||||||
|
version: 1.0.0
|
||||||
|
description: "Okta (Core API) — identity containment and investigation: suspend/unsuspend/deactivate/unlock users, clear active sessions and tokens, expire passwords, reset MFA factors, read users and factors, query the System Log, and manage group membership. API-token (SSWS) authentication; stdlib-only, no extra Python dependencies."
|
||||||
|
changelog: "1.0.0 — Initial release: user lifecycle (suspend/unsuspend/deactivate/unlock), clear sessions, expire password, reset factors, get/list users, get factors, system log, add/remove group membership."
|
||||||
|
category: identity
|
||||||
|
|
||||||
|
# Per-instance configuration. The API token is sent as 'Authorization: SSWS <api_token>'.
|
||||||
|
config_schema:
|
||||||
|
properties:
|
||||||
|
base_url:
|
||||||
|
type: string
|
||||||
|
description: "Okta org URL (e.g. https://acme.okta.com)"
|
||||||
|
api_token:
|
||||||
|
type: string
|
||||||
|
description: "Okta API token (SSWS)"
|
||||||
|
x-soar-sensitive: true
|
||||||
|
required:
|
||||||
|
- base_url
|
||||||
|
- api_token
|
||||||
|
|
||||||
|
commands:
|
||||||
|
- id: suspend_user
|
||||||
|
name: okta-suspend-user
|
||||||
|
description: "Suspend a user (blocks sign-in without deleting the account)."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "User ID or login" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: unsuspend_user
|
||||||
|
name: okta-unsuspend-user
|
||||||
|
description: "Unsuspend a user (restores an ACTIVE status)."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "User ID or login" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: deactivate_user
|
||||||
|
name: okta-deactivate-user
|
||||||
|
description: "Deactivate a user (stronger than suspend; requires reactivation to restore)."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "User ID or login" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: unlock_user
|
||||||
|
name: okta-unlock-user
|
||||||
|
description: "Unlock a user that was locked out by failed sign-ins."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "User ID or login" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: clear_user_sessions
|
||||||
|
name: okta-clear-user-sessions
|
||||||
|
description: "Revoke all of a user's active sessions and (optionally) OAuth tokens."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "User ID" }
|
||||||
|
revoke_oauth_tokens: { type: boolean, description: "Also revoke OAuth refresh/access tokens (default true)" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: expire_password
|
||||||
|
name: okta-expire-password
|
||||||
|
description: "Expire a user's password, forcing a reset at next sign-in."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "User ID or login" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: reset_factors
|
||||||
|
name: okta-reset-factors
|
||||||
|
description: "Reset (remove) all enrolled MFA factors for a user."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "User ID" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: get_user
|
||||||
|
name: okta-get-user
|
||||||
|
description: "Get a user's profile and status."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "User ID or login" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: list_users
|
||||||
|
name: okta-list-users
|
||||||
|
description: "List/search users (free-text query or a filter/search expression)."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
query: { type: string, description: "Free-text search of name/email/username (q)" }
|
||||||
|
filter: { type: string, description: "Okta filter expression (e.g. status eq \"ACTIVE\")" }
|
||||||
|
search: { type: string, description: "Okta search expression (SCIM-like)" }
|
||||||
|
limit: { type: number, description: "Max users (default 50)" }
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: get_user_factors
|
||||||
|
name: okta-get-user-factors
|
||||||
|
description: "List a user's enrolled MFA factors."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
user_id: { type: string, description: "User ID" }
|
||||||
|
required: [user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: get_logs
|
||||||
|
name: okta-get-logs
|
||||||
|
description: "Query the Okta System Log."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
query: { type: string, description: "Free-text query (q)" }
|
||||||
|
filter: { type: string, description: "SCIM filter expression" }
|
||||||
|
since: { type: string, description: "ISO-8601 start time" }
|
||||||
|
until: { type: string, description: "ISO-8601 end time" }
|
||||||
|
limit: { type: number, description: "Max events (default 100)" }
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: add_user_to_group
|
||||||
|
name: okta-add-user-to-group
|
||||||
|
description: "Add a user to a group."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
group_id: { type: string, description: "Group ID" }
|
||||||
|
user_id: { type: string, description: "User ID" }
|
||||||
|
required: [group_id, user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: remove_user_from_group
|
||||||
|
name: okta-remove-user-from-group
|
||||||
|
description: "Remove a user from a group."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
group_id: { type: string, description: "Group ID" }
|
||||||
|
user_id: { type: string, description: "User ID" }
|
||||||
|
required: [group_id, user_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
|
||||||
|
- id: test_connection
|
||||||
|
name: okta-test-connection
|
||||||
|
description: "Verify connectivity and the API token (used by the Test button)."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
group_id = inputs.get("group_id")
|
||||||
|
if not group_id:
|
||||||
|
raise Exception("group_id is required")
|
||||||
|
user_id = inputs.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
request(
|
||||||
|
"PUT",
|
||||||
|
"/groups/{}/users/{}".format(q(group_id), q(user_id)),
|
||||||
|
cfg,
|
||||||
|
body=None,
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = inputs.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
revoke_oauth_tokens = inputs.get("revoke_oauth_tokens")
|
||||||
|
if revoke_oauth_tokens is None:
|
||||||
|
revoke_oauth_tokens = True
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
request(
|
||||||
|
"DELETE",
|
||||||
|
"/users/{}/sessions".format(q(user_id)),
|
||||||
|
cfg,
|
||||||
|
body=None,
|
||||||
|
params={"oauthTokens": "true" if revoke_oauth_tokens else "false"},
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = inputs.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
request(
|
||||||
|
"POST",
|
||||||
|
"/users/{}/lifecycle/deactivate".format(q(user_id)),
|
||||||
|
cfg,
|
||||||
|
body=None,
|
||||||
|
params={"sendEmail": "false"},
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = inputs.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
return request("POST", "/users/{}/lifecycle/expire_password".format(q(user_id)), cfg, body=None)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
query = inputs.get("query")
|
||||||
|
filter_ = inputs.get("filter")
|
||||||
|
since = inputs.get("since")
|
||||||
|
until = inputs.get("until")
|
||||||
|
limit = inputs.get("limit")
|
||||||
|
limit = int(limit) if limit not in (None, "") else 100
|
||||||
|
return request(
|
||||||
|
"GET",
|
||||||
|
"/logs",
|
||||||
|
cfg,
|
||||||
|
params={"q": query, "filter": filter_, "since": since, "until": until, "limit": limit},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = inputs.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
return request("GET", "/users/{}".format(q(user_id)), cfg)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = inputs.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
return request("GET", "/users/{}/factors".format(q(user_id)), cfg)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
query = inputs.get("query")
|
||||||
|
filter_ = inputs.get("filter")
|
||||||
|
search = inputs.get("search")
|
||||||
|
limit = inputs.get("limit")
|
||||||
|
limit = int(limit) if limit not in (None, "") else 50
|
||||||
|
return request(
|
||||||
|
"GET",
|
||||||
|
"/users",
|
||||||
|
cfg,
|
||||||
|
params={"q": query, "filter": filter_, "search": search, "limit": limit},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
group_id = inputs.get("group_id")
|
||||||
|
if not group_id:
|
||||||
|
raise Exception("group_id is required")
|
||||||
|
user_id = inputs.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
request(
|
||||||
|
"DELETE",
|
||||||
|
"/groups/{}/users/{}".format(q(group_id), q(user_id)),
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = inputs.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
request("POST", "/users/{}/lifecycle/reset_factors".format(q(user_id)), cfg, body=None)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = inputs.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
request("POST", "/users/{}/lifecycle/suspend".format(q(user_id)), cfg, body=None)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
request("GET", "/users", cfg, params={"limit": 1})
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = inputs.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
request("POST", "/users/{}/lifecycle/unlock".format(q(user_id)), cfg, body=None)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
base = str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||||
|
url = base + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {
|
||||||
|
"Authorization": "SSWS " + str(cfg.get("api_token", "")),
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
print(json.dumps(fn(_cfg(), _inputs())))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(cfg, inputs):
|
||||||
|
user_id = inputs.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise Exception("user_id is required")
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
request("POST", "/users/{}/lifecycle/unsuspend".format(q(user_id)), cfg, body=None)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
Reference in New Issue
Block a user