ldap-rest 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/args.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/scim/errors.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/list.js +1 -0
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/scim/scim.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/twake/appAccountsApi.js +1 -1
- package/dist/plugins/twake/appAccountsConsistency.js +1 -1
- package/dist/src/config/args.d.ts +2 -0
- package/dist/src/lib/utils.d.ts +4 -0
- package/dist/src/plugins/scim/groups.d.ts +1 -0
- package/dist/src/plugins/scim/list.d.ts +39 -0
- package/dist/src/plugins/scim/mapping.d.ts +15 -0
- package/dist/src/plugins/scim/patch.d.ts +2 -1
- package/dist/src/plugins/scim/scim.d.ts +15 -0
- package/dist/src/plugins/scim/users.d.ts +1 -0
- package/dist/src/plugins/twake/appAccountsConsistency.d.ts +40 -2
- package/docs/usage/configuration.md +20 -5
- package/docs/usage/plugins/integrations/app-accounts.md +3 -2
- package/docs/usage/plugins/integrations/scim.md +54 -4
- package/package.json +5 -1
package/dist/config/args.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as _}from"url";import{join as a,dirname as e}from"path";const r=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",a(e(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",a(e(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",a(e(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{r as default};
|
|
1
|
+
import{fileURLToPath as _}from"url";import{join as a,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",a(e(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",a(e(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",a(e(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
|
package/dist/lib/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{BadRequestError as e}from"./errors.js";import{getLogger as t}from"./expressFormatedResponses.js";const n=t(),r=new Map;function getCompiledRegex(e,t){const n=t?`${e}:${t}`:e;let i=r.get(n);return i||(i=new RegExp(e,t),r.set(n,i)),i}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const launchHooks=async(e,...t)=>{if(e)for(const r of e)if(r)try{await r(...t)}catch(e){n.error("Hook error",e)}},launchHooksChained=async(e,t)=>{if(e)for(const n of e)n&&(t=await n(t));return t},transformSchemas=(e,t)=>e.toString().replace(/__(\S+)__/g,(e,n)=>{if(!n||"string"!=typeof n)return e;const r=n.trim().toLowerCase();return t[r]?"object"!=typeof t[r]?t[r]:JSON.stringify(t[r]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function escapeDnValue(e){return e.replace(/\\/g,"\\\\").replace(/,/g,"\\,").replace(/\+/g,"\\+").replace(/"/g,'\\"').replace(/</g,"\\<").replace(/>/g,"\\>").replace(/;/g,"\\;").replace(/=/g,"\\=").replace(/\0/g,"\\00").replace(/^\s/,"\\ ").replace(/\s$/,"\\ ").replace(/^#/,"\\#")}function unescapeDnValue(t){if("string"!=typeof t)throw new e("DN value must be a string");let n="",r=0;for(;r<t.length;)if("\\"===t[r]&&r+1<t.length){const e=t[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<t.length){const e=t.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){n+=String.fromCharCode(parseInt(e,16)),r+=3;continue}}n+=e,r+=2}else n+=t[r],r++;return n}function validateDnValue(e,t){if(null==e||"string"!=typeof e)throw new Error(`${t} must be a string`);if(0===e.trim().length)throw new Error(`${t} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${t} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${t} contains invalid invisible characters`)}function parseDn(t){if("string"!=typeof t)throw new e("DN must be a string");const n=[];let r="",i=!1;for(let e=0;e<t.length;e++){const o=t[e];i?(r+=o,i=!1):"\\"===o?(r+=o,i=!0):","===o?(n.push(r.trim()),r=""):r+=o}return r&&n.push(r.trim()),n}function getParentDn(e){const t=parseDn(e);return t.length<=1?e:t.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function isChildOf(e,t){const n=e.toLowerCase(),r=t.toLowerCase();if(!n.endsWith(r))return!1;if(n.length===r.length)return!1;return n.substring(0,n.length-r.length).endsWith(",")}function normalizeDn(e){return parseDn(e).map(e=>e.split("+").map(e=>{const t=e.indexOf("=");return(-1===t?e.trim():`${e.slice(0,t).trim()}=${e.slice(t+1).trim()}`).toLowerCase()}).sort().join("+")).filter(e=>e.length>0).join(",")}function isDnInBranch(e,t){const n=normalizeDn(t).split(",").filter(Boolean);if(0===n.length)return!1;const r=normalizeDn(e).split(",").filter(Boolean);if(r.length<n.length)return!1;return r.slice(r.length-n.length).every((e,t)=>e===n[t])}const asyncHandler=e=>(t,n,r)=>{Promise.resolve(e(t,n,r)).catch(r)};export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,launchHooks,launchHooksChained,normalizeDn,parseDn,transformSchemas,unescapeDnValue,validateDnValue};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{transformSchemas as o,asyncHandler as n,escapeDnValue as d,validateDnValue as h,launchHooksChained as c,launchHooks as p,getCompiledRegex as l,escapeRegex as m,escapeLdapFilter as u}from"../../lib/utils.js";import{NotFoundError as f,BadRequestError as g}from"../../lib/errors.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async([e,t])=>{let r=e;return Array.isArray(r)||(r=[r]),this.logger.debug(`User deletion detected, removing from groups: ${r.join(", ")}`),await Promise.all(r.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)}),[e,t]}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(o(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new g("Invalid match query");if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new g("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups(i);return a(t,s)})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,n(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new g("cn is required");const a=i(e,t,"member");a&&await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,n(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new g("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"targetOrgDn");o&&await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"newCn");o&&await s(t,this.renameGroup.bind(this),a,o.newCn)}))}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${d(i)},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)throw new f("Group not found");t.json(r.searchEntries[0])}catch(e){if(e.code&&32===e.code)throw new f("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;const o=a[this.cn],n=a.member?a.member:[],d=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,d)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new g("cn is required");await s(t,this.deleteGroup.bind(this),i)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new g("cn is required");const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(h(e,this.cn),i=`${this.cn}=${d(e)},${this.base}`),await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});const s=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||s.includes("twakeGroup")||s.includes("twakeStaticGroup")||s.push("twakeGroup");let a,o={objectClass:s,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&o.member.push(...t),[i,o]=await c(this.registeredHooks.ldapgroupadd,[i,o]);try{a=await this.ldap.add(i,o)}catch(e){throw new Error(`Failed to add group ${i}: ${e}`)}return p(this.registeredHooks.ldapgroupadddone,[i,o]),a}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;const i=this.opNumber();if([r,t]=await c(this.registeredHooks.ldapgroupmodify,[r,t,i]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(r,t);const s=await this.ldap.modify(r,t);return p(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){/,/.test(e)||h(e,this.cn),/,/.test(t)||h(t,this.cn);let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${d(t)},${this.base}`;[r,i]=await c(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return p(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;t=await c(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return p(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await c(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;if([e,t]=await c(this.registeredHooks.ldapgroupdeletemember,[e,t]),t===this.config.group_dummy_user)throw new Error("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${e}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r))))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let h;try{h=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===h.searchEntries.length)throw new Error(`Target organization ${t} not found`);const c=h.searchEntries[0][s];if(!c)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:c}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)e.searchEntries.map(e=>{const t=e[this.cn];t&&(i[t]=e),Array.isArray(i[t].member)||(i[t].member=[i[t].member]),i[t].member=i[t].member.filter(e=>e!==this.config.group_dummy_user)});return i=await c(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=t?`(${this.cn}=*${e}*)`:`(${this.cn}=${e})`;return await this.listGroups({filter:i,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await c(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await c(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new Error(`Invalid value for field ${e}`);for(const[e,r]of Object.entries(this.schema.attributes))if(r.required&&null==t[e])throw new Error(`Missing required field ${e}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await c(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new Error(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new Error(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new Error(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test){const i="string"==typeof r.items.test?l(r.items.test):r.items.test;for(let s of t){if(typeof s!==r.items.type)throw new Error(`Field ${e} must be of type ${r.items.type}`);if("string"!=typeof s&&(s=s.toString()),!i.test(s))throw new Error(`Field ${e} has invalid value ${s}`)}}}else if("pointer"===r.type){if("string"!=typeof t)throw new Error(`Field ${e} must be a string (DN pointer)`);const i=t;if(r.branch&&r.branch.length>0){if(!r.branch.some(e=>l(`,?${m(e)}$`,"i").test(i)))throw new Error(`Field ${e} must point to a DN within allowed branches: ${r.branch.join(", ")}`)}try{const t=await this.ldap.search({paged:!1,scope:"base"},i);if(!t||!t.searchEntries||0===t.searchEntries.length)throw new Error(`Field ${e} points to non-existent DN: ${i}`)}catch(t){throw new Error(`Field ${e} points to invalid or non-existent DN: ${i}: ${t}`)}if(r.test){if(!("string"==typeof r.test?l(r.test):r.test).test(i))throw new Error(`Field ${e} has invalid value ${i}`)}}else{if(typeof t!==r.type)return!1;if("string"!=typeof t&&(t=t.toString()),r.test){if(!("string"==typeof r.test?l(r.test):r.test).test(t))throw new Error(`Field ${e} has invalid value ${t}`)}}return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
|
|
1
|
+
import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{transformSchemas as o,asyncHandler as n,escapeDnValue as d,validateDnValue as h,launchHooksChained as c,launchHooks as p,getCompiledRegex as l,escapeRegex as m,escapeLdapFilter as u}from"../../lib/utils.js";import{NotFoundError as f,BadRequestError as g}from"../../lib/errors.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async([e,t])=>{let r=e;return Array.isArray(r)||(r=[r]),this.logger.debug(`User deletion detected, removing from groups: ${r.join(", ")}`),await Promise.all(r.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)}),[e,t]}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(o(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new g("Invalid match query");if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new g("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups(i);return a(t,s)})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,n(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new g("cn is required");const a=i(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new g("member must be a string or string array");await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,n(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new g("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new g("targetOrgDn must be a string");await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"newCn");if(o){if("string"!=typeof o.newCn)throw new g("newCn must be a string");await s(t,this.renameGroup.bind(this),a,o.newCn)}}))}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${d(i)},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)throw new f("Group not found");t.json(r.searchEntries[0])}catch(e){if(e.code&&32===e.code)throw new f("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;const o=a[this.cn],n=a.member?a.member:[],d=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,d)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new g("cn is required");await s(t,this.deleteGroup.bind(this),i)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new g("cn is required");const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(h(e,this.cn),i=`${this.cn}=${d(e)},${this.base}`),await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});const s=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||s.includes("twakeGroup")||s.includes("twakeStaticGroup")||s.push("twakeGroup");let a,o={objectClass:s,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&o.member.push(...t),[i,o]=await c(this.registeredHooks.ldapgroupadd,[i,o]);try{a=await this.ldap.add(i,o)}catch(e){throw new Error(`Failed to add group ${i}: ${e}`)}return p(this.registeredHooks.ldapgroupadddone,[i,o]),a}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;const i=this.opNumber();if([r,t]=await c(this.registeredHooks.ldapgroupmodify,[r,t,i]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(r,t);const s=await this.ldap.modify(r,t);return p(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){/,/.test(e)||h(e,this.cn),/,/.test(t)||h(t,this.cn);let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${d(t)},${this.base}`;[r,i]=await c(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return p(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;t=await c(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return p(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await c(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;if([e,t]=await c(this.registeredHooks.ldapgroupdeletemember,[e,t]),t===this.config.group_dummy_user)throw new Error("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${e}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r))))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let h;try{h=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===h.searchEntries.length)throw new Error(`Target organization ${t} not found`);const c=h.searchEntries[0][s];if(!c)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:c}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)e.searchEntries.map(e=>{const t=e[this.cn];t&&(i[t]=e),Array.isArray(i[t].member)||(i[t].member=[i[t].member]),i[t].member=i[t].member.filter(e=>e!==this.config.group_dummy_user)});return i=await c(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=t?`(${this.cn}=*${e}*)`:`(${this.cn}=${e})`;return await this.listGroups({filter:i,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await c(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await c(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new Error(`Invalid value for field ${e}`);for(const[e,r]of Object.entries(this.schema.attributes))if(r.required&&null==t[e])throw new Error(`Missing required field ${e}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await c(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new Error(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new Error(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new Error(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test){const i="string"==typeof r.items.test?l(r.items.test):r.items.test;for(let s of t){if(typeof s!==r.items.type)throw new Error(`Field ${e} must be of type ${r.items.type}`);if("string"!=typeof s&&(s=s.toString()),!i.test(s))throw new Error(`Field ${e} has invalid value ${s}`)}}}else if("pointer"===r.type){if("string"!=typeof t)throw new Error(`Field ${e} must be a string (DN pointer)`);const i=t;if(r.branch&&r.branch.length>0){if(!r.branch.some(e=>l(`,?${m(e)}$`,"i").test(i)))throw new Error(`Field ${e} must point to a DN within allowed branches: ${r.branch.join(", ")}`)}try{const t=await this.ldap.search({paged:!1,scope:"base"},i);if(!t||!t.searchEntries||0===t.searchEntries.length)throw new Error(`Field ${e} points to non-existent DN: ${i}`)}catch(t){throw new Error(`Field ${e} points to invalid or non-existent DN: ${i}: ${t}`)}if(r.test){if(!("string"==typeof r.test?l(r.test):r.test).test(i))throw new Error(`Field ${e} has invalid value ${i}`)}}else{if(typeof t!==r.type)return!1;if("string"!=typeof t&&(t=t.toString()),r.test){if(!("string"==typeof r.test?l(r.test):r.test).test(t))throw new Error(`Field ${e} has invalid value ${t}`)}}return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{HttpError as r}from"../../lib/errors.js";import{SCHEMA_ERROR as e}from"./types.js";const t="application/scim+json";class ScimError extends r{constructor(r,e,t){super(e,r),this.name="ScimError",this.scimType=t}}const scimInvalidFilter=(r="Invalid filter")=>new ScimError(400,r,"invalidFilter"),scimInvalidPath=(r="Invalid path")=>new ScimError(400,r,"invalidPath"),scimInvalidValue=(r="Invalid value")=>new ScimError(400,r,"invalidValue"),scimInvalidSyntax=(r="Invalid syntax")=>new ScimError(400,r,"invalidSyntax"),scimNoTarget=(r="No target")=>new ScimError(400,r,"noTarget"),scimMutability=(r="Immutable attribute")=>new ScimError(400,r,"mutability"),scimUniqueness=(r="Uniqueness violation")=>new ScimError(409,r,"uniqueness"),scimTooMany=(r="Too many results")=>new ScimError(400,r,"tooMany"),scimNotFound=(r="Resource not found")=>new ScimError(404,r);function writeScimError(r,o,
|
|
1
|
+
import{HttpError as r}from"../../lib/errors.js";import{SCHEMA_ERROR as e}from"./types.js";const t="application/scim+json";class ScimError extends r{constructor(r,e,t){super(e,r),this.name="ScimError",this.scimType=t}}const scimInvalidFilter=(r="Invalid filter")=>new ScimError(400,r,"invalidFilter"),scimInvalidPath=(r="Invalid path")=>new ScimError(400,r,"invalidPath"),scimInvalidValue=(r="Invalid value")=>new ScimError(400,r,"invalidValue"),scimInvalidSyntax=(r="Invalid syntax")=>new ScimError(400,r,"invalidSyntax"),scimNoTarget=(r="No target")=>new ScimError(400,r,"noTarget"),scimMutability=(r="Immutable attribute")=>new ScimError(400,r,"mutability"),scimUniqueness=(r="Uniqueness violation")=>new ScimError(409,r,"uniqueness"),scimTooMany=(r="Too many results")=>new ScimError(400,r,"tooMany"),scimNotFound=(r="Resource not found")=>new ScimError(404,r);function writeScimError(r,i,o,n){const s={schemas:[e],status:String(i),...n?{scimType:n}:{},detail:o};r.status(i).type(t).json(s)}function extractLdapCode(r){if(null==r)return;if("object"==typeof r&&"code"in r){const e=r.code;if("number"==typeof e)return e}const e=r instanceof Error?r.message:"string"==typeof r?r:"";return/noSuchObject|No such object|code:?\s*(32|0x20)/i.test(e)?32:/entryAlreadyExists|Already[_ ]?Exists|code:?\s*(68|0x44)/i.test(e)?68:/noSuchAttribute|No such attribute|code:?\s*(16|0x10)/i.test(e)?16:/sizeLimitExceeded|Size Limit Exceeded/i.test(e)?4:void 0}function writeScimErrorFromException(e,t,i=500){const sanitize=r=>/\[authz-forbidden\]/.test(r)?"Token does not have permission on this branch":r;if(t instanceof ScimError)return void writeScimError(e,t.statusCode,sanitize(t.message),t.scimType);if(t instanceof r)return void writeScimError(e,t.statusCode,sanitize(t.message));const o=t instanceof Error?t.message:String(t);if(/\[authz-forbidden\]/.test(o))return void writeScimError(e,403,"Token does not have permission on this branch");const n=extractLdapCode(t);32!==n?68!==n?writeScimError(e,i,sanitize(o)||"Internal error"):writeScimError(e,409,sanitize(o),"uniqueness"):writeScimError(e,404,"Resource not found")}const scimAsyncHandler=r=>(e,t,i)=>{Promise.resolve().then(()=>r(e,t,i)).catch(r=>{t.headersSent?i(r):writeScimErrorFromException(t,r)})};export{t as SCIM_CONTENT_TYPE,ScimError,extractLdapCode,scimAsyncHandler,scimInvalidFilter,scimInvalidPath,scimInvalidSyntax,scimInvalidValue,scimMutability,scimNoTarget,scimNotFound,scimTooMany,scimUniqueness,writeScimError,writeScimErrorFromException};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as r,isChildOf as i,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{mergeMapping as n,loadMappingFile as c,DEFAULT_GROUP_MAPPING as h,requiredLdapAttributes as u,ldapToScimGroup as m,scimGroupToLdap as
|
|
1
|
+
import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as r,isChildOf as i,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{mergeMapping as n,loadMappingFile as c,DEFAULT_GROUP_MAPPING as h,requiredLdapAttributes as u,ldapToScimGroup as m,scimGroupToLdap as d}from"./mapping.js";import{scimFilterToLdap as l}from"./filter.js";import{pagedSearch as p}from"./list.js";import{patchToModifyRequest as g}from"./patch.js";import{extractLdapCode as f,scimNotFound as b,ScimError as y,scimInvalidValue as w,scimUniqueness as _}from"./errors.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=n(h,e?c(e):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const e=this.baseResolver.userBase(t),s=this.baseResolver.groupBase(t),r=this.scimPrefix,i=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",o=this.rdnAttribute,n=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(t.toLowerCase()===n.toLowerCase())return;const c=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!c)return;const h=c[2].replace(/\\(.)/g,"$1"),u=t.toLowerCase();let m;e&&u.endsWith(e.toLowerCase())?m="User":s&&u.endsWith(s.toLowerCase())?m="Group":c[1]===a?m="User":c[1]===o&&(m="Group");const d={value:h};if(m&&(d.type=m,i)){const t="User"===m?"Users":"Groups";d.$ref=`${i.replace(/\/$/,"")}${r}/${t}/${encodeURIComponent(h)}`}return d}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.search({paged:!1,scope:"base",attributes:[...u(this.mapping),"member"]},s,t)}catch(t){if(32===f(t))throw b(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw b(`Group ${e} not found`);return m(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let a,n=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=l(e.filter,this.mapping);t.idEquals?a=t.idEquals:n=`(&${n}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof y&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:c,totalResults:h}=await p({ldap:this.ldap,base:s,filter:n,attributes:[...u(this.mapping),"member"],startIndex:r,count:i,maxScanned:this.maxScanned,req:t}),d=this.buildMemberResolver(t),g=c.map(e=>m(e,this.mapping,this.ctx(t),d));return{schemas:[o],totalResults:h,startIndex:r,itemsPerPage:g.length,Resources:g}}async create(i,a){if(!a.displayName)throw w("displayName is required");const o=(await e(this.hooks.scimgroupcreate,[a,i]))[0],{rdn:n,attributes:c}=d(o,this.mapping,this.objectClass);if(!n)throw w("displayName is required");s(n,this.rdnAttribute),c[this.rdnAttribute]=n;const h=[];if(o.members&&Array.isArray(o.members))for(const t of o.members){if(!t||!t.value)continue;const e=await this.users.resolveRef(i,t.value);e&&h.push(e)}if(0===h.length){const t=this.config.group_dummy_user||"cn=fakeuser";h.push(t)}c.member=h;const u=this.baseResolver.groupBase(i),m=`${this.rdnAttribute}=${t(n)},${u}`;try{await this.ldap.add(m,c,i)}catch(t){if(68===f(t))throw _(`Group ${n} already exists`);throw t}const l=await this.get(i,n);return r(this.hooks.scimgroupcreatedone,l),l}async replace(t,s,i){await this.get(t,s);const a=(await e(this.hooks.scimgroupupdate,[s,i,t]))[1],{attributes:o}=d(a,this.mapping,this.objectClass),n=[];if(a.members&&Array.isArray(a.members))for(const e of a.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(s,t),h={replace:{}};for(const[t,e]of Object.entries(o))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.modify(c,h,t);const u=await this.get(t,s);return r(this.hooks.scimgroupupdatedone,s,u),u}async patch(t,e,s){await this.get(t,e);const i=await g(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e)});if(!i.add&&!i.replace&&(!i.delete||Array.isArray(i.delete)&&0===i.delete.length))return this.get(t,e);const a=this.config.group_dummy_user||"cn=fakeuser",o=this.dnForId(e,t);if(i.delete&&!Array.isArray(i.delete)&&i.delete.member){const e=await this.ldap.search({paged:!1,scope:"base",attributes:["member"]},o,t),s=e.searchEntries[0]?.member,r=Array.isArray(s)?s:s?[String(s)]:[],n=Array.isArray(i.delete.member)?i.delete.member:[String(i.delete.member)];0===r.filter(t=>!n.includes(t)).length&&(i.add||(i.add={}),i.add.member=[a])}await this.ldap.modify(o,i,t);const n=await this.get(t,e);return r(this.hooks.scimgroupupdatedone,e,n),n}async delete(t,s){await this.get(t,s);const i=(await e(this.hooks.scimgroupdelete,[s,t]))[0],a=this.dnForId(i,t);await this.ldap.delete(a,t),r(this.hooks.scimgroupdeletedone,i)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e);if(s.includes("=")&&s.includes(","))return i(s,r)?s:void 0;const o=`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},o,e);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==f(t))throw t}try{const t=await this.ldap.search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r,e);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==f(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{scimTooMany as t,ScimError as e,extractLdapCode as r}from"./errors.js";async function pagedSearch(a){const i=Math.max(0,a.startIndex-1),s=[];let o=0;try{const e=await a.ldap.search({filter:a.filter,scope:"sub",paged:{pageSize:(n=a.count,Math.min(500,Math.max(n,100)))},attributes:a.attributes,sizeLimit:0},a.base,a.req);for await(const r of e)for(const e of r.searchEntries||[])if(o>=i&&s.length<a.count&&s.push(e),o++,o>a.maxScanned)throw t(`Query matched more than ${a.maxScanned} resources; narrow it with a filter or raise --scim-max-scanned`)}catch(a){if(a instanceof e)throw a;const i=r(a);if(32===i)return{entries:[],totalResults:0};if(4===i)throw t("The directory refused the query with sizeLimitExceeded; narrow it with a filter or raise the directory size limit");throw a}var n;return{entries:s,totalResults:o}}export{pagedSearch};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import{SCHEMA_GROUP as r,SCHEMA_USER as
|
|
1
|
+
import e from"fs";import{SCHEMA_GROUP as r,SCHEMA_USER as t}from"./types.js";const i=["createTimestamp","modifyTimestamp","entryUUID"],a={resourceType:"User",schemas:[t],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},n={resourceType:"Group",schemas:[r],entries:[{scim:"displayName",ldap:"cn"},{scim:"externalId",ldap:"entryUUID",operational:!0,readOnly:!0}]};function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,i,a,n,s,o,l,c]=r;let d=0;if(l){const e=o?1e3:s?6e4:36e5;d=Math.round(Number(`0.${l}`)*e)}let p=1e3*(60*(60*Number(n)+Number(s||0))+Number(o||0))+d;const m=Math.floor(p/36e5);p-=36e5*m;const u=Math.floor(p/6e4);p-=6e4*u;const f=Math.floor(p/1e3),y=p-1e3*f;let S="Z";c&&"Z"!==c&&(S=`${c[0]}${c.slice(1,3)}:${c.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${i}-${a}T${pad(m)}:${pad(u)}:${pad(f)}${y?`.${String(y).padStart(3,"0")}`:""}${S}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const t="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${t}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,i){const a={schemas:[t]},n=resolveScimId(e,i);n&&(a.id=n);for(const t of r.entries){if(t.sub){const r={};for(const[i,a]of Object.entries(t.sub)){const t=asString(e[a]);null!=t&&(r[i]=t)}Object.keys(r).length>0&&(a[t.scim]=r);continue}if(t.ldapPrimary||t.ldapSecondary){const r=[];if(t.ldapPrimary){const i=asString(e[t.ldapPrimary]);null!=i&&r.push({value:i,primary:!0})}if(t.ldapSecondary)for(const i of asArray(e[t.ldapSecondary]))r.push({value:i});r.length>0&&(a[t.scim]=r);continue}if(t.ldap)if("array"===t.multi){const r=asArray(e[t.ldap]);r.length>0&&(a[t.scim]=r)}else{const r=asString(e[t.ldap]);null!=r&&(a[t.scim]=r)}}return a.active=null==e.pwdAccountLockedTime,n&&(a.meta={resourceType:"User",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(n,i)}),a}function scimUserToLdap(e,r,t,i){const a={objectClass:i},n=e.userName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;const r=e[t.scim];if(null!=r)if(t.sub&&"object"==typeof r)for(const[e,i]of Object.entries(t.sub)){const t=r[e];"string"==typeof t&&t.length>0&&(a[i]=t)}else{if(t.ldapPrimary||t.ldapSecondary){if(!Array.isArray(r))continue;const e=r,i=e.find(e=>!0===e.primary)||e[0],n=e.filter(e=>e!==i);t.ldapPrimary&&i&&i.value&&(a[t.ldapPrimary]=i.value),t.ldapSecondary&&n.length>0&&(a[t.ldapSecondary]=n.map(e=>e.value).filter(Boolean));continue}t.ldap&&(Array.isArray(r)?a[t.ldap]=r.map(e=>String(e)):"string"!=typeof r&&"number"!=typeof r||(a[t.ldap]=String(r)))}}if(!a.cn&&n&&(a.cn=n),!a.sn){const r=e.name?.familyName||e.displayName||n;r&&(a.sn=r)}return{rdn:n,attributes:a}}function ldapToScimGroup(e,t,i,a){const n={schemas:[r]},s=resolveScimId(e,i);s&&(n.id=s);for(const r of t.entries){if(!r.ldap)continue;const t=e[r.ldap];if(null!=t)if("array"===r.multi){const e=asArray(t);e.length>0&&(n[r.scim]=e)}else{const e=asString(t);null!=e&&(n[r.scim]=e)}}const o=asArray(e.member),l=[];for(const e of o)if(e)if(a){const r=a(e);r&&l.push(r)}else l.push({value:e,type:"User"});return l.length>0&&(n.members=l),s&&(n.meta={resourceType:"Group",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(s,i)}),n}function scimGroupToLdap(e,r,t){const i={objectClass:t},a=e.displayName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;if(!t.ldap)continue;const r=e[t.scim];null!=r&&(Array.isArray(r)?i[t.ldap]=r.map(e=>"string"==typeof e?e:JSON.stringify(e)):"string"!=typeof r&&"number"!=typeof r||(i[t.ldap]=String(r)))}return{rdn:a,attributes:i}}function requiredLdapAttributes(e){const r=new Set(["objectClass",...i]);for(const t of e.entries)if(t.ldap&&r.add(t.ldap),t.ldapPrimary&&r.add(t.ldapPrimary),t.ldapSecondary&&r.add(t.ldapSecondary),t.sub)for(const e of Object.values(t.sub))r.add(e);return Array.from(r)}function scimPathToLdapAttribute(e,r){const t=e.split(".")[0],i=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===t){if(e.sub&&i)return e.sub[i];if((e.ldapPrimary||e.ldapSecondary)&&("value"===i||!i))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{n as DEFAULT_GROUP_MAPPING,a as DEFAULT_USER_MAPPING,buildLocation,ldapTimeToIso,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,requiredLdapAttributes,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{scimInvalidValue as e,scimInvalidPath as t,scimNoTarget as r}from"./errors.js";import{scimPathToLdapAttribute as o}from"./mapping.js";function normalizeOp(t){const r=t.toLowerCase();if("add"===r||"remove"===r||"replace"===r)return r;throw e(`Unknown patch op '${t}'`)}const a=new Set(["__proto__","constructor","prototype"]),n=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(a.has(e))throw t(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw t(`Malformed path '${String(e)}'`);let r,o,a;const i=e.indexOf("[");if(i>=0){const n=e.lastIndexOf("]");if(n<=i)throw t(`Malformed path '${e}'`);r=e.slice(0,i),o=e.slice(i+1,n);const l=e.slice(n+1);if(l.length>0){if("."!==l[0])throw t(`Malformed path '${e}'`);a=l.slice(1)}}else{const t=e.indexOf(".");t>=0?(r=e.slice(0,t),a=e.slice(t+1)):r=e}if(!n.test(r))throw t(`Malformed path '${e}'`);if(assertSafeKey(r),null!=a){if(!n.test(a))throw t(`Malformed sub-attribute in '${e}'`);assertSafeKey(a)}return{top:r,filter:o,sub:a}}function mergeAttr(e,t,r){const o=e[t];if(null==o)return void(e[t]=r);const a=[...(n=o,Array.isArray(n)?n.map(e=>"string"==typeof e?e:e.toString()):["string"==typeof n?n:n.toString()]),...Array.isArray(r)?r:[r]];var n;e[t]=a}async function applyOperation(a,n,i){const l=normalizeOp(a.op),s=i.memberAttribute||"member";if(!a.path){if(null==a.value||"object"!=typeof a.value||Array.isArray(a.value))throw r("PATCH without path requires an object value");const e=a.value;for(const[t,r]of Object.entries(e))await applyOperation({op:a.op,path:t,value:r},n,i);return}const{top:f,sub:p,filter:u}=parsePath(a.path);if("members"===f){if(!i.resolveMemberRef)throw r("Member operations require a member resolver");if("add"===l){const e=Array.isArray(a.value)?a.value:[a.value],t=[];for(const r of e){const e="string"==typeof r?r:"object"==typeof r&&null!=r&&"value"in r?String(r.value):"";if(!e)continue;const o=await i.resolveMemberRef(e);o&&t.push(o)}return void(t.length>0&&(n.add||(n.add={}),mergeAttr(n.add,s,t)))}if("remove"===l){const collectFromValue=()=>{if(null==a.value)return[];return(Array.isArray(a.value)?a.value:[a.value]).map(e=>"string"==typeof e?e:"object"==typeof e&&null!=e&&"value"in e?String(e.value):"").filter(Boolean)},e=[...(()=>{if(!u)return[];const e=/value\s+eq\s+"([^"]+)"/i.exec(u);return e?[e[1]]:[]})(),...collectFromValue()],t=[];for(const r of e){const e=await i.resolveMemberRef(r);e&&t.push(e)}return void(t.length>0?(n.delete||(n.delete={}),Array.isArray(n.delete)?n.delete={[s]:t}:mergeAttr(n.delete,s,t)):u||a.value||(n.delete||(n.delete={}),Array.isArray(n.delete)?n.delete.push(s):n.delete[s]=""))}if("replace"===l){const e=Array.isArray(a.value)?a.value:[a.value],t=[];for(const r of e){const e="string"==typeof r?r:"object"==typeof r&&null!=r&&"value"in r?String(r.value):"";if(!e)continue;const o=await i.resolveMemberRef(e);o&&t.push(o)}return void(t.length>0&&(n.replace||(n.replace={}),n.replace[s]=t))}}if(u)throw t(`Complex multi-valued filters are only supported on 'members' (got '${a.path}')`);const c=o(p?`${f}.${p}`:f,i.mapping);if(!c)throw t(`Unknown SCIM attribute path '${a.path}'`);if("remove"===l)return n.delete||(n.delete={}),void(Array.isArray(n.delete)?n.delete.push(c):n.delete[c]="");const y=function(t){if(null!=t)return"string"==typeof t?t:"number"==typeof t||"boolean"==typeof t?String(t):Array.isArray(t)?t.map(t=>{if("string"==typeof t)return t;if("number"==typeof t||"boolean"==typeof t)return String(t);if(t&&"object"==typeof t&&!Array.isArray(t)&&"value"in t&&("string"==typeof t.value||"number"==typeof t.value||"boolean"==typeof t.value))return String(t.value);throw e(`Unsupported array element in PATCH value: ${JSON.stringify(t)}`)}):void 0}(a.value);if(null==y)throw e(`PATCH ${a.op} ${a.path} missing value`);return"add"===l?(n.add||(n.add={}),void mergeAttr(n.add,c,y)):"replace"===l?(n.replace||(n.replace={}),void(n.replace[c]=y)):void 0}async function patchToModifyRequest(t,r){if(!t.Operations||!Array.isArray(t.Operations))throw e("Missing Operations array");const o={};for(const e of t.Operations)await applyOperation(e,o,r);return o}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:a}=parsePath(e.path);if("remove"!==t)if(a){const t=r[o],n=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);n[a]=e.value,r[o]=n}else r[o]=e.value;else if(a){const e=r[o];e&&"object"==typeof e&&delete e[a]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest};
|
|
1
|
+
import{scimInvalidValue as e,scimInvalidPath as t,scimNoTarget as r}from"./errors.js";import{scimPathToLdapAttribute as o}from"./mapping.js";function normalizeOp(t){const r=t.toLowerCase();if("add"===r||"remove"===r||"replace"===r)return r;throw e(`Unknown patch op '${t}'`)}const a=new Set(["__proto__","constructor","prototype"]),n=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(a.has(e))throw t(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw t(`Malformed path '${String(e)}'`);let r,o,a;const i=e.indexOf("[");if(i>=0){const n=e.lastIndexOf("]");if(n<=i)throw t(`Malformed path '${e}'`);r=e.slice(0,i),o=e.slice(i+1,n);const l=e.slice(n+1);if(l.length>0){if("."!==l[0])throw t(`Malformed path '${e}'`);a=l.slice(1)}}else{const t=e.indexOf(".");t>=0?(r=e.slice(0,t),a=e.slice(t+1)):r=e}if(!n.test(r))throw t(`Malformed path '${e}'`);if(assertSafeKey(r),null!=a){if(!n.test(a))throw t(`Malformed sub-attribute in '${e}'`);assertSafeKey(a)}return{top:r,filter:o,sub:a}}function mergeAttr(e,t,r){const o=e[t];if(null==o)return void(e[t]=r);const a=[...(n=o,Array.isArray(n)?n.map(e=>"string"==typeof e?e:e.toString()):["string"==typeof n?n:n.toString()]),...Array.isArray(r)?r:[r]];var n;e[t]=a}async function applyOperation(a,n,i){const l=normalizeOp(a.op),s=i.memberAttribute||"member";if(!a.path){if("remove"===l)throw r("PATCH 'remove' requires a path");if(null==a.value||"object"!=typeof a.value||Array.isArray(a.value))throw r("PATCH without path requires an object value");const e=a.value;for(const[t,r]of Object.entries(e))await applyOperation({op:a.op,path:t,value:r},n,i);return}const{top:f,sub:p,filter:u}=parsePath(a.path);if("members"===f){if(!i.resolveMemberRef)throw r("Member operations require a member resolver");if("add"===l){const e=Array.isArray(a.value)?a.value:[a.value],t=[];for(const r of e){const e="string"==typeof r?r:"object"==typeof r&&null!=r&&"value"in r?String(r.value):"";if(!e)continue;const o=await i.resolveMemberRef(e);o&&t.push(o)}return void(t.length>0&&(n.add||(n.add={}),mergeAttr(n.add,s,t)))}if("remove"===l){const collectFromValue=()=>{if(null==a.value)return[];return(Array.isArray(a.value)?a.value:[a.value]).map(e=>"string"==typeof e?e:"object"==typeof e&&null!=e&&"value"in e?String(e.value):"").filter(Boolean)},e=[...(()=>{if(!u)return[];const e=/value\s+eq\s+"([^"]+)"/i.exec(u);return e?[e[1]]:[]})(),...collectFromValue()],t=[];for(const r of e){const e=await i.resolveMemberRef(r);e&&t.push(e)}return void(t.length>0?(n.delete||(n.delete={}),Array.isArray(n.delete)?n.delete={[s]:t}:mergeAttr(n.delete,s,t)):u||a.value||(n.delete||(n.delete={}),Array.isArray(n.delete)?n.delete.push(s):n.delete[s]=""))}if("replace"===l){const e=Array.isArray(a.value)?a.value:[a.value],t=[];for(const r of e){const e="string"==typeof r?r:"object"==typeof r&&null!=r&&"value"in r?String(r.value):"";if(!e)continue;const o=await i.resolveMemberRef(e);o&&t.push(o)}return void(t.length>0&&(n.replace||(n.replace={}),n.replace[s]=t))}}if(u)throw t(`Complex multi-valued filters are only supported on 'members' (got '${a.path}')`);const c=o(p?`${f}.${p}`:f,i.mapping);if(!c)throw t(`Unknown SCIM attribute path '${a.path}'`);if("remove"===l)return n.delete||(n.delete={}),void(Array.isArray(n.delete)?n.delete.push(c):n.delete[c]="");const y=function(t){if(null!=t)return"string"==typeof t?t:"number"==typeof t||"boolean"==typeof t?String(t):Array.isArray(t)?t.map(t=>{if("string"==typeof t)return t;if("number"==typeof t||"boolean"==typeof t)return String(t);if(t&&"object"==typeof t&&!Array.isArray(t)&&"value"in t&&("string"==typeof t.value||"number"==typeof t.value||"boolean"==typeof t.value))return String(t.value);throw e(`Unsupported array element in PATCH value: ${JSON.stringify(t)}`)}):void 0}(a.value);if(null==y)throw e(`PATCH ${a.op} ${a.path} missing value`);return"add"===l?(n.add||(n.add={}),void mergeAttr(n.add,c,y)):"replace"===l?(n.replace||(n.replace={}),void(n.replace[c]=y)):void 0}async function patchToModifyRequest(t,r){if(!t.Operations||!Array.isArray(t.Operations))throw e("Missing Operations array");const o={};for(const e of t.Operations)await applyOperation(e,o,r);return o}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:a}=parsePath(e.path);if("remove"!==t)if(a){const t=r[o],n=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);n[a]=e.value,r[o]=n}else r[o]=e.value;else if(a){const e=r[o];e&&"object"==typeof e&&delete e[a]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as s}from"url";import{join as
|
|
1
|
+
import{fileURLToPath as s}from"url";import{join as e,dirname as i}from"path";import t from"body-parser";import o from"../../abstract/plugin.js";import{BaseResolver as r}from"./baseResolver.js";import{ScimUsers as a}from"./users.js";import{ScimGroups as c}from"./groups.js";import{ScimBulk as n}from"./bulk.js";import{ScimDiscovery as p}from"./discovery.js";import{SCIM_CONTENT_TYPE as m,scimAsyncHandler as d,writeScimError as u}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";class Scim extends o{constructor(t){super(t),this.name="scim",this.roles=["api","configurable"],this.ldap=t.ldap,this.scimPrefix=this.config.scim_prefix||"/scim/v2",this.baseResolver=new r(this.config),this.users=new a({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,hooks:this.registeredHooks}),this.groups=new c({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hooks:this.registeredHooks}),this.bulk=new n({config:this.config,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hooks:this.registeredHooks});const o=this.config.schemas_path||e(i(s(import.meta.url)),"..","..","..","static","schemas");this.discovery=new p({config:this.config,schemaDir:e(o,"scim"),scimPrefix:this.scimPrefix,loadedPlugins:t.loadedPlugins})}scimJson(s,e,i){s.status(e).type(m).json(i)}scimResource(s,e,i,t){const o=i.id?`${this.scimPrefix}/${t}/${encodeURIComponent(i.id)}`:void 0,r=(this.config.scim_base_url||"")&&i.meta?.location?i.meta.location:o;r&&s.set("Location",r),this.scimJson(s,e,i)}readScimBody(s){const e=String(s.headers["content-type"]||"");if(e.startsWith("application/scim+json")||e.startsWith("application/json"))return s.body}api(s){const e=this.scimPrefix;s.use(e,t.json({type:["application/json","application/scim+json"],limit:`${this.config.scim_bulk_max_payload_size||1048576}b`})),s.get(`${e}/Users`,d(async(s,e)=>{const i=this.parseListQuery(s),t=await this.users.list(s,i);this.scimJson(e,200,t)})),s.get(`${e}/Users/:id`,d(async(s,e)=>{const i=await this.users.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,i)})),s.post(`${e}/Users`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return u(e,400,"Missing body","invalidSyntax");const t=await this.users.create(s,i);this.scimResource(e,201,t,"Users")})),s.put(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return u(e,400,"Missing body","invalidSyntax");const t=await this.users.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users")})),s.patch(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return u(e,400,"Missing body","invalidSyntax");const t=await this.users.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users")})),s.delete(`${e}/Users/:id`,d(async(s,e)=>{await this.users.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.get(`${e}/Groups`,d(async(s,e)=>{const i=this.parseListQuery(s),t=await this.groups.list(s,i);this.scimJson(e,200,t)})),s.get(`${e}/Groups/:id`,d(async(s,e)=>{const i=await this.groups.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,i)})),s.post(`${e}/Groups`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return u(e,400,"Missing body","invalidSyntax");const t=await this.groups.create(s,i);this.scimResource(e,201,t,"Groups")})),s.put(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return u(e,400,"Missing body","invalidSyntax");const t=await this.groups.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups")})),s.patch(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return u(e,400,"Missing body","invalidSyntax");const t=await this.groups.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups")})),s.delete(`${e}/Groups/:id`,d(async(s,e)=>{await this.groups.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.post(`${e}/Bulk`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return u(e,400,"Missing body","invalidSyntax");const t=this.config.scim_bulk_max_payload_size||1048576,o=JSON.stringify(i).length;if(o>t)return u(e,413,`Bulk payload too large: ${o} > ${t}`,"invalidValue");const r=await this.bulk.execute(s,i);this.scimJson(e,200,r)})),s.get(`${e}/ServiceProviderConfig`,d((s,e)=>{this.scimJson(e,200,this.discovery.serviceProviderConfig(s))})),s.get(`${e}/ResourceTypes`,d((s,e)=>{const i=this.discovery.resourceTypes(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/ResourceTypes/:name`,d((s,e)=>{const i=this.discovery.resourceType(s.params.name,s);if(!i)return u(e,404,"ResourceType not found");this.scimJson(e,200,i)})),s.get(`${e}/Schemas`,d((s,e)=>{const i=this.discovery.schemas(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/Schemas/:id`,d((s,e)=>{const i=this.discovery.schema(decodeURIComponent(s.params.id),s);if(!i)return u(e,404,"Schema not found");this.scimJson(e,200,i)}))}parseListQuery(s){const e=s.query,toInt=s=>{if("string"!=typeof s)return;const e=parseInt(s,10);return Number.isFinite(e)?e:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},i="string"==typeof e.sortOrder?e.sortOrder:void 0;return{filter:"string"==typeof e.filter?e.filter:void 0,startIndex:toInt(e.startIndex),count:toInt(e.count),attributes:toCsv(e.attributes),excludedAttributes:toCsv(e.excludedAttributes),sortBy:"string"==typeof e.sortBy?e.sortBy:void 0,sortOrder:"ascending"===i||"descending"===i?i:void 0}}getConfigApiData(){const s=this.scimPrefix;let e;if(this.server.loadedPlugins.static){const s=this.config.static_name||"static";e={user:`/${s}/schemas/scim/User.json`,group:`/${s}/schemas/scim/Group.json`,defaultMapping:`/${s}/schemas/scim/default-mapping.json`}}const i=this.users.userMapping,t=this.groups.groupMapping,o=i.entries.map(s=>s.scim).concat(["id","active"]),r=t.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:e,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:Boolean(this.config.scim_etag),changePassword:!1,maxResults:this.config.scim_max_results||200,maxScanned:this.config.scim_max_scanned||1e4,bulkMaxOperations:this.config.scim_bulk_max_operations||100,bulkMaxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.config.scim_user_rdn_attribute||"uid",objectClass:this.config.scim_user_object_class,filterableAttributes:o,mapping:i.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.config.scim_group_rdn_attribute||"cn",objectClass:this.config.scim_group_object_class,filterableAttributes:r,mapping:t.entries}],idStrategy:this.config.scim_id_attribute||"rdn",baseResolution:{userBaseTemplate:this.config.scim_user_base_template||void 0,groupBaseTemplate:this.config.scim_group_base_template||void 0,hasBaseMap:Boolean(this.config.scim_base_map)}}}}export{Scim as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeDnValue as t,launchHooksChained as s,validateDnValue as e,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as
|
|
1
|
+
import{escapeDnValue as t,launchHooksChained as s,validateDnValue as e,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{mergeMapping as o,loadMappingFile as h,DEFAULT_USER_MAPPING as c,requiredLdapAttributes as l,ldapToScimUser as d,scimUserToLdap as u}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{pagedSearch as m}from"./list.js";import{patchToModifyRequest as g}from"./patch.js";import{extractLdapCode as f,scimNotFound as b,ScimError as w,scimInvalidValue as y,scimUniqueness as x}from"./errors.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const s=this.config.scim_user_mapping||"";this.mapping=o(c,s?h(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix}}dnForId(s,e){const i=this.baseResolver.userBase(e);return`${this.rdnAttribute}=${t(s)},${i}`}async get(t,s){const e=this.dnForId(s,t);let i;try{i=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},e,t)}catch(t){if(32===f(t))throw b(`User ${s} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw b(`User ${s} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,s){const e=this.baseResolver.userBase(t),i=Math.max(1,s.startIndex||1),r=Math.min(this.maxResults,Math.max(0,s.count??this.maxResults));let a,o=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(s.filter){const t=p(s.filter,this.mapping);t.idEquals?a=t.idEquals:o=`(&${o}${t.ldapFilter})`}if(a)try{const s=await this.get(t,a);return{schemas:[n],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[s]}}catch(t){if(t instanceof w&&404===t.statusCode)return{schemas:[n],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:h,totalResults:c}=await m({ldap:this.ldap,base:e,filter:o,attributes:l(this.mapping),startIndex:i,count:r,maxScanned:this.maxScanned,req:t});s.sortBy,s.sortOrder;const u=h.map(s=>d(s,this.mapping,this.ctx(t)));return{schemas:[n],totalResults:c,startIndex:i,itemsPerPage:u.length,Resources:u}}async create(r,a){if(!a.userName)throw y("userName is required");const n=(await s(this.hooks.scimusercreate,[a,r]))[0],{rdn:o,attributes:h}=u(n,this.mapping,this.ctx(r),this.objectClass);if(!o)throw y("userName is required");e(o,this.rdnAttribute),h[this.rdnAttribute]=o;const c=this.baseResolver.userBase(r),l=`${this.rdnAttribute}=${t(o)},${c}`;try{await this.ldap.add(l,h,r)}catch(t){if(68===f(t))throw x(`User ${o} already exists`);throw t}const d=await this.get(r,o);return i(this.hooks.scimusercreatedone,d),d}async replace(t,e,r){const a=this.dnForId(e,t),n=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},a,t);if(!n.searchEntries?.length)throw b(`User ${e} not found`);const o=n.searchEntries[0],h=(await s(this.hooks.scimuserupdate,[e,r,t]))[1],{attributes:c}=u(h,this.mapping,this.ctx(t),this.objectClass),d={replace:{},delete:[]},p=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of l(this.mapping))p.has(t)||(null!=c[t]?d.replace[t]=c[t]:hasAttrValue(o[t])&&d.delete.push(t));if(0===Object.keys(d.replace||{}).length&&delete d.replace,0===d.delete.length&&delete d.delete,d.replace||d.delete)try{await this.ldap.modify(a,d,t)}catch(t){if(16!==f(t))throw t}const m=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,m),m}async patch(t,s,e){await this.get(t,s);const r=await g(e,{mapping:this.mapping});if(!r.add&&!r.replace&&(!r.delete||Array.isArray(r.delete)&&0===r.delete.length))return this.get(t,s);const a=this.dnForId(s,t);await this.ldap.modify(a,r,t);const n=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,n),n}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimuserdelete,[e,t]))[0],a=this.dnForId(r,t);await this.ldap.delete(a,t),i(this.hooks.scimuserdeletedone,r)}async resolveRef(s,e){if(!e)return;const i=this.baseResolver.userBase(s);if(e.includes("=")&&e.includes(","))return r(e,i)?e:void 0;const n=`${this.rdnAttribute}=${t(e)},${i}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},n,s);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==f(t))throw t}try{const t=await this.ldap.search({filter:`(${this.rdnAttribute}=${a(e)})`,scope:"sub",paged:!1,attributes:["dn"]},i,s);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==f(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";import{escapeLdapFilter as e,isDnInBranch as i,escapeDnValue as a}from"../../lib/utils.js";class AppAccountsApi extends r{constructor(t){if(super(t),this.name="appAccountsApi",this.roles=["api","configurable"],this.dependencies={appAccountsConsistency:"core/twake/appAccountsConsistency"},this.applicativeAccountBase=this.config.applicative_account_base,this.maxAppAccounts=this.config.max_app_accounts||5,this.mailAttr=this.config.mail_attribute||"mail",this.userAttr=this.config.app_accounts_user_attribute||this.mailAttr,!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}, max_app_accounts=${this.maxAppAccounts}, user_attribute=${this.userAttr}`)}api(t){const r=this.config.api_prefix||"/api";t.get(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.listAccounts(t,r)),t.post(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.createAccount(t,r)),t.delete(`${r}/v1/users/:user/app-accounts/:uid`,(t,r)=>this.deleteAccount(t,r)),this.logger.info(`AppAccounts API registered at ${r}/v1/users/:user/app-accounts`)}async resolvePrincipal(t,r,s){const a=((await this.server.ldap.search({scope:"sub",filter:`(${this.userAttr}=${e(t)})`,paged:!1,...s?{attributes:s}:{}},this.config.ldap_base||"")).searchEntries||[]).filter(t=>!i(t.dn,this.applicativeAccountBase));return 0===a.length?(r.status(404).json({error:`User ${t} not found`}),null):a.length>1?(this.logger.error(`${this.name}: Ambiguous principal lookup for ${t}: ${a.length} entries share ${this.userAttr}`),r.status(409).json({error:`Multiple users share ${this.userAttr} ${t}`}),null):a[0]}async listAccounts(t,r){if(!s(t,r))return;const i=t.params.user;try{const t=await this.resolvePrincipal(i,r,[this.mailAttr]);if(!t)return;const s=t[this.mailAttr];if(!s)return void r.status(400).json({error:`User ${i} has no mail attribute`});const a=Array.isArray(s)?String(s[0]):String(s),o=await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${e(a)})`,paged:!1},this.applicativeAccountBase),n=(o.searchEntries||[]).filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==a.toLowerCase()}).map(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r),e=t.description,i=e?Array.isArray(e)?String(e[0]):String(e):void 0;return{uid:s,...i?{name:i}:{}}}).sort((t,r)=>t.uid.localeCompare(r.uid));r.json(n)}catch(t){this.logger.error(`${this.name}: Failed to list accounts for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}async createAccount(t,r){if(!s(t,r))return;const i=t.params.user,{name:o}=t.body||{};try{const t=await this.resolvePrincipal(i,r);if(!t)return;const s=t[this.mailAttr];if(!s)return void r.status(400).json({error:`User ${i} has no mail attribute`});const n=Array.isArray(s)?String(s[0]):String(s),c=this.appUidPrefix(i),u=(await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${e(n)})`,paged:!1},this.applicativeAccountBase)).searchEntries||[];if(u.filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==n.toLowerCase()}).length>=this.maxAppAccounts)return void r.status(400).json({error:`Maximum number of accounts (${this.maxAppAccounts}) reached`});let p,l=0;do{if(p=`${c}_${this.generateAccountId()}`,l++,l>100)throw new Error("Failed to generate unique account ID after 100 attempts")}while(await this.uidExists(p));const h=this.generatePassword(),d={objectClass:["inetOrgPerson"],uid:p,userPassword:h},A=[
|
|
1
|
+
import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";import{escapeLdapFilter as e,isDnInBranch as i,escapeDnValue as a}from"../../lib/utils.js";class AppAccountsApi extends r{constructor(t){if(super(t),this.name="appAccountsApi",this.roles=["api","configurable"],this.dependencies={appAccountsConsistency:"core/twake/appAccountsConsistency"},this.applicativeAccountBase=this.config.applicative_account_base,this.maxAppAccounts=this.config.max_app_accounts||5,this.mailAttr=this.config.mail_attribute||"mail",this.userAttr=this.config.app_accounts_user_attribute||this.mailAttr,!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}, max_app_accounts=${this.maxAppAccounts}, user_attribute=${this.userAttr}`)}api(t){const r=this.config.api_prefix||"/api";t.get(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.listAccounts(t,r)),t.post(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.createAccount(t,r)),t.delete(`${r}/v1/users/:user/app-accounts/:uid`,(t,r)=>this.deleteAccount(t,r)),this.logger.info(`AppAccounts API registered at ${r}/v1/users/:user/app-accounts`)}async resolvePrincipal(t,r,s){const a=((await this.server.ldap.search({scope:"sub",filter:`(${this.userAttr}=${e(t)})`,paged:!1,...s?{attributes:s}:{}},this.config.ldap_base||"")).searchEntries||[]).filter(t=>!i(t.dn,this.applicativeAccountBase));return 0===a.length?(r.status(404).json({error:`User ${t} not found`}),null):a.length>1?(this.logger.error(`${this.name}: Ambiguous principal lookup for ${t}: ${a.length} entries share ${this.userAttr}`),r.status(409).json({error:`Multiple users share ${this.userAttr} ${t}`}),null):a[0]}async listAccounts(t,r){if(!s(t,r))return;const i=t.params.user;try{const t=await this.resolvePrincipal(i,r,[this.mailAttr]);if(!t)return;const s=t[this.mailAttr];if(!s)return void r.status(400).json({error:`User ${i} has no mail attribute`});const a=Array.isArray(s)?String(s[0]):String(s),o=await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${e(a)})`,paged:!1},this.applicativeAccountBase),n=(o.searchEntries||[]).filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==a.toLowerCase()}).map(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r),e=t.description,i=e?Array.isArray(e)?String(e[0]):String(e):void 0;return{uid:s,...i?{name:i}:{}}}).sort((t,r)=>t.uid.localeCompare(r.uid));r.json(n)}catch(t){this.logger.error(`${this.name}: Failed to list accounts for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}async createAccount(t,r){if(!s(t,r))return;const i=t.params.user,{name:o}=t.body||{};try{const t=await this.resolvePrincipal(i,r);if(!t)return;const s=t[this.mailAttr];if(!s)return void r.status(400).json({error:`User ${i} has no mail attribute`});const n=Array.isArray(s)?String(s[0]):String(s),c=this.appUidPrefix(i),u=(await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${e(n)})`,paged:!1},this.applicativeAccountBase)).searchEntries||[];if(u.filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==n.toLowerCase()}).length>=this.maxAppAccounts)return void r.status(400).json({error:`Maximum number of accounts (${this.maxAppAccounts}) reached`});let p,l=0;do{if(p=`${c}_${this.generateAccountId()}`,l++,l>100)throw new Error("Failed to generate unique account ID after 100 attempts")}while(await this.uidExists(p));const h=this.generatePassword(),d={objectClass:["inetOrgPerson"],uid:p,userPassword:h},A=[...new Set([...this.config.applicative_account_attribute||[],this.mailAttr])].filter(t=>"objectClass"!==t&&"description"!==t);for(const r of A)if(t[r]){const s=t[r];Array.isArray(s)?d[r]=s.map(t=>Buffer.isBuffer(t)?t.toString():String(t)):d[r]=Buffer.isBuffer(s)?s.toString():String(s)}o&&(d.description=o);const g=`uid=${a(p)},${this.applicativeAccountBase}`;await this.server.ldap.add(g,d),this.logger.info(`${this.name}: Created applicative account ${g} for user ${i}`);const f=`uid=${a(n)},${this.applicativeAccountBase}`;try{await this.server.ldap.modify(f,{add:{userPassword:h}})}catch(t){this.logger.error(`${this.name}: Failed to add password to principal account ${f}:`,t);try{await this.server.ldap.delete(g)}catch(t){this.logger.error(`${this.name}: Failed to roll back applicative account ${g}:`,t)}return void r.status(500).json({error:"Failed to register the password on the principal account"})}r.json({uid:p,pwd:h,mail:n})}catch(t){this.logger.error(`${this.name}: Failed to create account for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}appUidPrefix(t){return t.replace(/[^A-Za-z0-9_-]/g,"_")}async uidExists(t){return((await this.server.ldap.search({scope:"sub",filter:`(uid=${e(t)})`,paged:!1,attributes:["uid"]},this.applicativeAccountBase)).searchEntries||[]).length>0}async deleteAccount(t,r){if(!s(t,r))return;const i=t.params.user,o=t.params.uid;try{const t=await this.server.ldap.search({scope:"sub",filter:`(uid=${e(o)})`,paged:!1,attributes:["uid",this.mailAttr,"userPassword"]},this.applicativeAccountBase),s=t.searchEntries?.[0];if(!s)return this.logger.warn(`${this.name}: Account ${o} already deleted or not found`),void r.json({uid:o});const n=s[this.mailAttr],c=n?Array.isArray(n)?String(n[0]):String(n):null;let u=i;if(this.userAttr!==this.mailAttr){const t=await this.resolvePrincipal(i,r,[this.mailAttr]);if(!t)return;const s=t[this.mailAttr],e=s?Array.isArray(s)?String(s[0]):String(s):null;if(!e)return void r.status(400).json({error:`User ${i} has no mail attribute`});u=e}if(!c||c.toLowerCase()!==u.toLowerCase())return void r.status(403).json({error:`Account ${o} does not belong to user ${i}`});const p=s.userPassword;if(!p)return this.logger.error(`${this.name}: Account ${o} has no readable userPassword, refusing to delete: the bind DN needs read access on userPassword in the applicative branch`),void r.status(500).json({error:`Cannot revoke account ${o}: its password is not readable`});const l=`uid=${a(c)},${this.applicativeAccountBase}`;try{const t=Array.isArray(p)?p[0]:p;await this.server.ldap.modify(l,{delete:{userPassword:t}})}catch(t){const s=String(t.message??"");if(16!==t.code&&!/no ?such ?attribute/i.test(s))return this.logger.error(`${this.name}: Failed to delete password from principal account ${l}:`,t),void r.status(500).json({error:`Failed to revoke the password of account ${o}`});this.logger.warn(`${this.name}: Password of ${o} was already absent from principal account ${l}, resuming deletion`)}const h=s.dn;await this.server.ldap.delete(h),this.logger.info(`${this.name}: Deleted applicative account ${h}`),r.json({uid:o})}catch(t){this.logger.error(`${this.name}: Failed to delete account ${o} for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}generateAccountId(){return`c${t(0,1e8).toString().padStart(8,"0")}`}generatePassword(){const r="ABCDEFGHJKMNPQRSTUVWXYZ",s="abcdefghjkmnpqrstuvwxyz",e="23456789",i="@!#$%",a=[];for(let o=0;o<6;o++){const o=t(0,23),n=t(0,23),c=t(0,8),u=t(0,5),p=[r[o],s[n],e[c],i[u]];for(let r=p.length-1;r>0;r--){const s=t(0,r+1);[p[r],p[s]]=[p[s],p[r]]}a.push(p.join(""))}return a.join("-")}getConfigApiData(){const t=this.config.api_prefix||"/api";return{enabled:!0,base:this.applicativeAccountBase,maxAccounts:this.maxAppAccounts,mailAttribute:this.mailAttr,endpoints:{list:`${t}/v1/users/:user/app-accounts`,create:`${t}/v1/users/:user/app-accounts`,delete:`${t}/v1/users/:user/app-accounts/:uid`}}}}export{AppAccountsApi as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";import{isDnInBranch as e,escapeDnValue as i}from"../../lib/utils.js";class AppAccountsConsistency extends t{constructor(t){
|
|
1
|
+
import t from"../../abstract/plugin.js";import{isDnInBranch as e,escapeDnValue as i}from"../../lib/utils.js";function isNoSuchObject(t){if("object"!=typeof t||null===t)return!1;const{code:e,name:i,message:a}=t;return 32===e||"NoSuchObjectError"===i||"string"==typeof a&&a.includes("Code: 0x20")}class AppAccountsConsistency extends t{constructor(t){super(t),this.name="appAccountsConsistency",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={onLdapMailChange:async(t,e,i)=>{if(this.isInApplicativeBranch(t))this.logger.debug(`${this.name}: Ignoring mail change event originating in the applicative branch`);else try{const a=null!=e?Array.isArray(e)?String(e[0]):String(e):null,c=null!=i?Array.isArray(i)?String(i[0]):String(i):null;if(!a&&c)return void await this.createApplicativeAccount(t,c);if(a&&!c)return void await this.deleteApplicativeAccount(a);if(a&&c&&a!==c)return void await this.updateApplicativeAccount(t,a,c)}catch(e){this.logger.error(`${this.name}: Failed to handle mail change for ${t}:`,e)}}},this.mailAttr=this.config.mail_attribute||"mail",this.applicativeAccountBase=this.config.applicative_account_base,this.operationalAttributes=this.config.ldap_operational_attribute||[];const e=this.config.applicative_account_attribute||[],i=["objectClass",this.mailAttr],a=i.filter(t=>!e.includes(t));if(e.length>0&&a.length>0&&this.logger.warn(`${this.name}: applicative_account_attribute omits ${a.join(", ")} — copied anyway, an applicative entry cannot be created without them`),this.copiedAttributes=new Set([...e,...i]),!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}`)}shouldExcludeAttribute(t){return"dn"===t||this.operationalAttributes.includes(t)}async deleteApplicativeEntry(t){try{await this.server.ldap.delete(t),this.logger.info(`${this.name}: Deleted applicative account ${t}`)}catch(e){if(isNoSuchObject(e))return void this.logger.debug(`${this.name}: Applicative account ${t} already deleted`);throw e}}pickCopiedAttributes(t){const e={};for(const[i,a]of Object.entries(t))this.copiedAttributes.has(i)&&null!=a&&(Array.isArray(a)&&0===a.length||(e[i]=a));return e}isInApplicativeBranch(t){return e(t,this.applicativeAccountBase)}async createApplicativeAccount(t,e){const a=`uid=${i(e)},${this.applicativeAccountBase}`;try{const i=await this.server.ldap.search({scope:"base",paged:!1},t),c=i.searchEntries?.[0];if(!c)return void this.logger.warn(`${this.name}: Could not find user ${t} to create applicative account`);const s=this.pickCopiedAttributes(c);s.uid=e,await this.server.ldap.add(a,s),this.logger.info(`${this.name}: Created applicative account ${a} for user ${t}`)}catch(e){if(68===e.code||e.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${a} already exists`);this.logger.error(`${this.name}: Failed to create applicative account for ${t}:`,e)}}async deleteApplicativeAccount(t){try{const e=`(${this.mailAttr}=${t})`,i=(await this.server.ldap.search({filter:e,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===i.length)return void this.logger.debug(`${this.name}: No applicative accounts found for mail ${t}`);for(const t of i){const e=t.dn;try{await this.deleteApplicativeEntry(e)}catch(t){this.logger.error(`${this.name}: Failed to delete applicative account ${e}:`,t)}}}catch(e){if(isNoSuchObject(e))return void this.logger.debug(`${this.name}: Applicative account base does not exist or no accounts found for mail ${t}`);this.logger.error(`${this.name}: Failed to delete applicative account for mail ${t}:`,e)}}async updateApplicativeAccount(t,e,a){try{const c=`(${this.mailAttr}=${e})`,s=(await this.server.ldap.search({filter:c,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===s.length)return void this.logger.debug(`${this.name}: No applicative account found for old mail ${e}`);for(const c of s){const s=c.dn,r=Array.isArray(c.uid)?String(c.uid[0]):String(c.uid);if(!(r===e)){await this.deleteApplicativeEntry(s);continue}const n=`uid=${i(a)},${this.applicativeAccountBase}`;try{const i={};for(const[t,e]of Object.entries(c))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(i[t]=e));await this.deleteApplicativeEntry(s);const r={...i},o=await this.server.ldap.search({scope:"base",paged:!1},t),l=o.searchEntries?.[0];if(!l)return void this.logger.warn(`${this.name}: Could not find user ${t} to update applicative account`);Object.assign(r,this.pickCopiedAttributes(l)),r.uid=a,await this.server.ldap.add(n,r),this.logger.info(`${this.name}: Created new applicative account ${n} (mail changed from ${e} to ${a})`)}catch(t){if(68===t.code||t.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${n} already exists after mail change`);this.logger.error(`${this.name}: Failed to update applicative account for mail change:`,t)}}}catch(e){this.logger.error(`${this.name}: Failed to update applicative account for ${t}:`,e)}}}export{AppAccountsConsistency as default};
|
|
@@ -160,6 +160,7 @@ export interface Config {
|
|
|
160
160
|
applicative_account_base?: string;
|
|
161
161
|
max_app_accounts?: number;
|
|
162
162
|
app_accounts_user_attribute?: string;
|
|
163
|
+
applicative_account_attribute?: string[];
|
|
163
164
|
ldap_operational_attribute?: string[];
|
|
164
165
|
trash_base?: string;
|
|
165
166
|
trash_watched_bases?: string;
|
|
@@ -191,6 +192,7 @@ export interface Config {
|
|
|
191
192
|
scim_user_mapping?: string;
|
|
192
193
|
scim_group_mapping?: string;
|
|
193
194
|
scim_max_results?: number;
|
|
195
|
+
scim_max_scanned?: number;
|
|
194
196
|
scim_bulk_max_operations?: number;
|
|
195
197
|
scim_bulk_max_payload_size?: number;
|
|
196
198
|
scim_etag?: boolean;
|
package/dist/src/lib/utils.d.ts
CHANGED
|
@@ -73,6 +73,8 @@ export declare function escapeDnValue(value: string): string;
|
|
|
73
73
|
* unescapeDnValue('user\\+admin')
|
|
74
74
|
* // => 'user+admin'
|
|
75
75
|
* ```
|
|
76
|
+
*
|
|
77
|
+
* @throws BadRequestError if `value` is not a string
|
|
76
78
|
*/
|
|
77
79
|
export declare function unescapeDnValue(value: string): string;
|
|
78
80
|
/**
|
|
@@ -108,6 +110,8 @@ export declare function validateDnValue(value: string, fieldName: string): void;
|
|
|
108
110
|
* parseDn('cn=Smith\\, John,ou=users,dc=example,dc=com')
|
|
109
111
|
* // => ['cn=Smith\\, John', 'ou=users', 'dc=example', 'dc=com']
|
|
110
112
|
* ```
|
|
113
|
+
*
|
|
114
|
+
* @throws BadRequestError if `dn` is not a string
|
|
111
115
|
*/
|
|
112
116
|
export declare function parseDn(dn: string): string[];
|
|
113
117
|
/**
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/scim/list
|
|
3
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
+
*
|
|
5
|
+
* Server-side pagination for the SCIM list endpoints (RFC 7644 §3.4.2.4).
|
|
6
|
+
*
|
|
7
|
+
* A SCIM list is a window — `startIndex` (1-based) and `count` — over the full
|
|
8
|
+
* set of matching resources, plus the size of that set as `totalResults`. LDAP
|
|
9
|
+
* has no equivalent of an offset, so the window is cut while walking a paged
|
|
10
|
+
* search: entries before `startIndex` are counted and dropped, `count` of them
|
|
11
|
+
* are kept, and the remainder is counted so `totalResults` is real.
|
|
12
|
+
*
|
|
13
|
+
* Only the requested page is ever materialised, so the cost of a list is the
|
|
14
|
+
* page, not the directory. Walking still has to reach the end of the result
|
|
15
|
+
* set to count it, which is bounded by `maxScanned`: beyond that we answer the
|
|
16
|
+
* `tooMany` RFC 7644 §3.12 provides for a filter yielding "many more results
|
|
17
|
+
* than the server is willing to calculate or process".
|
|
18
|
+
*/
|
|
19
|
+
import type ldapActions from '../../lib/ldapActions';
|
|
20
|
+
import type { AttributesList } from '../../lib/ldapActions';
|
|
21
|
+
import type { DmRequest } from '../../lib/auth/base';
|
|
22
|
+
export interface PagedSearch {
|
|
23
|
+
ldap: ldapActions;
|
|
24
|
+
base: string;
|
|
25
|
+
filter: string;
|
|
26
|
+
attributes: string[];
|
|
27
|
+
/** 1-based index of the first resource to return. */
|
|
28
|
+
startIndex: number;
|
|
29
|
+
/** Page size. Zero is legal and asks for `totalResults` only. */
|
|
30
|
+
count: number;
|
|
31
|
+
/** Upper bound on entries walked before answering `tooMany`. */
|
|
32
|
+
maxScanned: number;
|
|
33
|
+
req?: DmRequest;
|
|
34
|
+
}
|
|
35
|
+
export interface PagedResult {
|
|
36
|
+
entries: AttributesList[];
|
|
37
|
+
totalResults: number;
|
|
38
|
+
}
|
|
39
|
+
export declare function pagedSearch(opts: PagedSearch): Promise<PagedResult>;
|
|
@@ -2,6 +2,21 @@ import type { AttributesList } from '../../lib/ldapActions';
|
|
|
2
2
|
import { type ScimUser, type ScimGroup, type ResourceMapping, type MultiValued } from './types';
|
|
3
3
|
export declare const DEFAULT_USER_MAPPING: ResourceMapping;
|
|
4
4
|
export declare const DEFAULT_GROUP_MAPPING: ResourceMapping;
|
|
5
|
+
/**
|
|
6
|
+
* LDAP GeneralizedTime (RFC 4517 §3.3.13) → SCIM DateTime (RFC 7643 §2.3.5,
|
|
7
|
+
* an `xsd:dateTime`).
|
|
8
|
+
*
|
|
9
|
+
* The directory answers `createTimestamp`/`modifyTimestamp` as
|
|
10
|
+
* `YYYYMMDDHHMMSSZ` (optionally with a fraction, a `+HHMM` offset, or the
|
|
11
|
+
* seconds and minutes omitted). SCIM clients parse `meta.created` as an
|
|
12
|
+
* `xsd:dateTime` and reject anything else, so the raw directory value must not
|
|
13
|
+
* be forwarded as-is.
|
|
14
|
+
*
|
|
15
|
+
* A value that does not look like a GeneralizedTime is returned untouched: a
|
|
16
|
+
* directory may already store an ISO 8601 string, and dropping it would lose
|
|
17
|
+
* more than passing it through.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ldapTimeToIso(value: string | undefined): string | undefined;
|
|
5
20
|
export declare function loadMappingFile(path: string): ResourceMapping | undefined;
|
|
6
21
|
/**
|
|
7
22
|
* Build the mapping used at runtime by merging a user-supplied JSON
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* - sub-attribute paths: "name.familyName"
|
|
11
11
|
* - multi-valued paths: "emails"
|
|
12
12
|
* - Group member ops via filtered path: `members[value eq "alice"]`
|
|
13
|
-
* - implicit path (op.value is an object)
|
|
13
|
+
* - implicit path (op.value is an object) for add and replace; `remove`
|
|
14
|
+
* without a path is rejected with `noTarget` per RFC 7644 section 3.5.2.2
|
|
14
15
|
*
|
|
15
16
|
* Complex filtered paths on multi-valued attributes OTHER than `members`
|
|
16
17
|
* (e.g. `emails[type eq "work"]`) are rejected with `invalidPath`: the
|
|
@@ -404,6 +404,21 @@ export default class Scim extends DmPlugin {
|
|
|
404
404
|
private readonly baseResolver;
|
|
405
405
|
constructor(server: DM);
|
|
406
406
|
private scimJson;
|
|
407
|
+
/**
|
|
408
|
+
* Answer a single resource, adding the `Location` response header required
|
|
409
|
+
* by RFC 7644 section 3.1 for a create and used there for a modification.
|
|
410
|
+
*
|
|
411
|
+
* The header is absolute only when the operator pinned the external base
|
|
412
|
+
* URL with `--scim-base-url`. Without it, `meta.location` is built from the
|
|
413
|
+
* request's own `Host`, which the client controls: echoing that into a
|
|
414
|
+
* redirect header would let a caller choose where another consumer of the
|
|
415
|
+
* response is pointed. So we emit the prefix-relative URI instead — valid
|
|
416
|
+
* per RFC 7231 section 7.1.2, and it still names the resource.
|
|
417
|
+
*
|
|
418
|
+
* `meta.location` inside the body keeps its existing behaviour; pin
|
|
419
|
+
* `--scim-base-url` to make it absolute and trustworthy too.
|
|
420
|
+
*/
|
|
421
|
+
private scimResource;
|
|
407
422
|
private readScimBody;
|
|
408
423
|
api(app: Express): void;
|
|
409
424
|
private parseListQuery;
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
*
|
|
21
21
|
* When a user is deleted, all corresponding applicative accounts are also deleted.
|
|
22
22
|
*
|
|
23
|
-
* When a user's mail changes,
|
|
23
|
+
* When a user's mail changes, the principal account moves to the new address and
|
|
24
|
+
* the app accounts are deleted: every client has to be reconfigured against the
|
|
25
|
+
* new address anyway.
|
|
24
26
|
*/
|
|
25
27
|
import DmPlugin from '../../abstract/plugin';
|
|
26
28
|
import type { Role } from '../../abstract/plugin';
|
|
@@ -35,13 +37,49 @@ export default class AppAccountsConsistency extends DmPlugin {
|
|
|
35
37
|
private mailAttr;
|
|
36
38
|
private applicativeAccountBase;
|
|
37
39
|
private operationalAttributes;
|
|
40
|
+
private copiedAttributes;
|
|
38
41
|
constructor(server: DM);
|
|
39
42
|
/**
|
|
40
|
-
* Check if an attribute should be excluded when
|
|
43
|
+
* Check if an attribute should be excluded when recreating an existing
|
|
44
|
+
* applicative entry from its own current state.
|
|
45
|
+
*
|
|
46
|
+
* This is a denylist because the input is an entry we wrote ourselves: what
|
|
47
|
+
* has to go is what the directory generates and would refuse on an `add`,
|
|
48
|
+
* plus `userPassword` (an app account is reconfigured on every client after
|
|
49
|
+
* a mail change anyway, so its password is deliberately not carried over).
|
|
50
|
+
*
|
|
51
|
+
* Attributes coming from the *user* entry are chosen by
|
|
52
|
+
* {@link pickCopiedAttributes} instead — an allowlist, so that a new
|
|
53
|
+
* attribute on the user schema is never propagated by accident.
|
|
54
|
+
*
|
|
41
55
|
* @param key - The attribute name to check
|
|
42
56
|
* @returns true if the attribute should be excluded
|
|
43
57
|
*/
|
|
44
58
|
private shouldExcludeAttribute;
|
|
59
|
+
/**
|
|
60
|
+
* Delete an entry of the applicative branch, tolerating its absence.
|
|
61
|
+
*
|
|
62
|
+
* A missing entry is the state we wanted, so `NoSuchObject` is not an error:
|
|
63
|
+
* it keeps the mail-change path idempotent when a hook fires twice.
|
|
64
|
+
*
|
|
65
|
+
* @param dn - DN of the applicative entry to remove
|
|
66
|
+
* @throws whatever the directory raised, except NoSuchObject
|
|
67
|
+
*/
|
|
68
|
+
private deleteApplicativeEntry;
|
|
69
|
+
/**
|
|
70
|
+
* Select the attributes to copy from a user entry into an applicative entry.
|
|
71
|
+
*
|
|
72
|
+
* An allowlist (`--applicative-account-attribute`, plus `objectClass` and the
|
|
73
|
+
* mail attribute, which are forced in by the constructor):
|
|
74
|
+
* the applicative branch is only ever read to bind with `uid` and
|
|
75
|
+
* `userPassword`, so anything else the user entry happens to carry —
|
|
76
|
+
* recovery addresses, e2ee key material, whatever the schema grows next —
|
|
77
|
+
* has no reason to be duplicated there.
|
|
78
|
+
*
|
|
79
|
+
* @param entry - The source user entry
|
|
80
|
+
* @returns The subset worth copying, empty values dropped
|
|
81
|
+
*/
|
|
82
|
+
private pickCopiedAttributes;
|
|
45
83
|
/**
|
|
46
84
|
* Whether a DN belongs to the applicative branch this plugin manages.
|
|
47
85
|
*
|
|
@@ -322,14 +322,29 @@ Empty means the plugin guards every path. Scoping several instances to different
|
|
|
322
322
|
|
|
323
323
|
#### `core/twake/applicativeAccounts`
|
|
324
324
|
|
|
325
|
-
| CLI
|
|
326
|
-
|
|
|
327
|
-
| `--applicative-account-base`
|
|
328
|
-
| `--max-app-accounts`
|
|
329
|
-
| `--
|
|
325
|
+
| CLI | Plural | Env | Default | Description |
|
|
326
|
+
| --------------------------------- | ---------------------------------- | ----------------------------------- | ------------- | ------------------------------------- |
|
|
327
|
+
| `--applicative-account-base` | | `DM_APPLICATIVE_ACCOUNT_BASE` | | Applicative accounts base DN |
|
|
328
|
+
| `--max-app-accounts` | | `DM_MAX_APP_ACCOUNTS` | `5` | Max accounts per user |
|
|
329
|
+
| `--applicative-account-attribute` | `--applicative-account-attributes` | `DM_APPLICATIVE_ACCOUNT_ATTRIBUTES` | _(see below)_ | Attributes copied from the user entry |
|
|
330
|
+
| `--ldap-operational-attribute` | `--ldap-operational-attributes` | `DM_LDAP_OPERATIONAL_ATTRIBUTES` | _(see below)_ | Operational attributes to exclude |
|
|
331
|
+
|
|
332
|
+
Default copied attributes: `objectClass`, `cn`, `sn`, `givenName`, `displayName`, `description` — plus the configured mail attribute, always.
|
|
333
|
+
|
|
334
|
+
This is an **allowlist**: an attribute the user entry carries but that is not
|
|
335
|
+
named here is not copied into the applicative branch. That branch is only ever
|
|
336
|
+
read to bind with `uid` and `userPassword`, so anything else has no reason to
|
|
337
|
+
be duplicated there — and a new attribute added to the user schema later stays
|
|
338
|
+
out unless someone opts in.
|
|
330
339
|
|
|
331
340
|
Default operational attributes: `dn`, `controls`, `structuralObjectClass`, `entryUUID`, `entryDN`, `subschemaSubentry`, `modifyTimestamp`, `modifiersName`, `createTimestamp`, `creatorsName`, `userPassword`
|
|
332
341
|
|
|
342
|
+
Operational attributes are only used when an applicative entry is **recreated
|
|
343
|
+
from its own current state** (on a mail change): they are what the directory
|
|
344
|
+
generates and would refuse on an `add`. `userPassword` is in that list on
|
|
345
|
+
purpose — a mail change forces every client to be reconfigured anyway, so app
|
|
346
|
+
accounts are deliberately reissued without a password.
|
|
347
|
+
|
|
333
348
|
#### `core/twake/appAccountsConsistency`
|
|
334
349
|
|
|
335
350
|
Automatically creates/updates/deletes applicative account entries when users are created/modified/deleted. No configuration options - uses [core/twake/applicativeAccounts](#coretwakeapplicativeaccounts) settings.
|
|
@@ -43,7 +43,8 @@ Instead of using a single primary password to access all services that authentic
|
|
|
43
43
|
2. **App accounts consistency plugin** `core/twake/appAccountsConsistency` - **Required**
|
|
44
44
|
- Automatically creates principal accounts (uid=mail@domain.com)
|
|
45
45
|
- Ensures automatic cleanup when users are deleted
|
|
46
|
-
-
|
|
46
|
+
- Moves the principal account on a mail change, and drops the app accounts:
|
|
47
|
+
every MUA has to be reconfigured against the new address anyway
|
|
47
48
|
3. **Applicative accounts base** configured in LDAP (e.g., `ou=applicative,dc=example,dc=com`)
|
|
48
49
|
4. **Read access on `userPassword`** in that branch, for the bind DN
|
|
49
50
|
(`--ldap-dn`) - **Required for deletion**
|
|
@@ -295,7 +296,7 @@ sync-app-accounts --help
|
|
|
295
296
|
- Secure random password
|
|
296
297
|
4. Plugin creates applicative account in LDAP:
|
|
297
298
|
- `uid=alice_c12345678,ou=applicative,dc=example,dc=com`
|
|
298
|
-
- Copies attributes from user (
|
|
299
|
+
- Copies the allowlisted attributes from the user (`--applicative-account-attribute`)
|
|
299
300
|
- Sets generated password
|
|
300
301
|
5. Plugin adds password to principal account:
|
|
301
302
|
- `uid=alice@example.com,ou=applicative,dc=example,dc=com`
|
|
@@ -56,12 +56,29 @@ All SCIM endpoints sit behind the auth middleware registered before the plugin
|
|
|
56
56
|
| `--scim-id-attribute` | `DM_SCIM_ID_ATTRIBUTE` | `rdn` | `rdn` (default) or `entryUUID` for SCIM `id` |
|
|
57
57
|
| `--scim-user-mapping` | `DM_SCIM_USER_MAPPING` | — | Path to JSON mapping override |
|
|
58
58
|
| `--scim-group-mapping` | `DM_SCIM_GROUP_MAPPING` | — | Same for Groups |
|
|
59
|
-
| `--scim-max-results` | `DM_SCIM_MAX_RESULTS` | `200` |
|
|
59
|
+
| `--scim-max-results` | `DM_SCIM_MAX_RESULTS` | `200` | Maximum page size (caps `count`) |
|
|
60
|
+
| `--scim-max-scanned` | `DM_SCIM_MAX_SCANNED` | `10000` | Entries a list may walk before answering `400 tooMany` |
|
|
60
61
|
| `--scim-bulk-max-operations` | `DM_SCIM_BULK_MAX_OPERATIONS` | `100` | Max `/Bulk` operations per request |
|
|
61
62
|
| `--scim-bulk-max-payload-size` | `DM_SCIM_BULK_MAX_PAYLOAD_SIZE` | `1048576` | Max `/Bulk` payload size in bytes |
|
|
62
63
|
| `--scim-etag` | `DM_SCIM_ETAG` | `false` | Advertise ETag support in discovery (not yet implemented) |
|
|
63
64
|
| `--scim-base-url` | `DM_SCIM_BASE_URL` | auto from request | Override external base URL for `meta.location` values |
|
|
64
65
|
|
|
66
|
+
### Authorization
|
|
67
|
+
|
|
68
|
+
Every SCIM operation now reaches `ldapActions` with the request attached, so
|
|
69
|
+
the authorization plugins (`core/auth/authzPerBranch`,
|
|
70
|
+
`core/auth/authzDynamic`, …) apply to reads as well as writes. An identity
|
|
71
|
+
denied `read` on a branch gets `403` from `GET /Users`, `GET /Users/{id}` and
|
|
72
|
+
their Group counterparts, instead of the data.
|
|
73
|
+
|
|
74
|
+
An identity that writes needs `read` too: every SCIM write answers with the
|
|
75
|
+
resource it just changed, so it reads the entry back. A write-only grant
|
|
76
|
+
cannot be served, and answers 403 on the read-back.
|
|
77
|
+
|
|
78
|
+
Base resolution (see _multi-tenant_ below) is unchanged and independent: it
|
|
79
|
+
confines an identity to its own subtree whether or not an authorization plugin
|
|
80
|
+
is loaded.
|
|
81
|
+
|
|
65
82
|
### Authentication
|
|
66
83
|
|
|
67
84
|
SCIM piggybacks on whichever auth plugin you load. The discovery endpoint
|
|
@@ -247,6 +264,39 @@ via `escapeLdapFilter()` — no LDAP injection is possible.
|
|
|
247
264
|
|
|
248
265
|
Unknown SCIM attributes raise `400 invalidFilter`.
|
|
249
266
|
|
|
267
|
+
## Pagination
|
|
268
|
+
|
|
269
|
+
`GET /Users` and `GET /Groups` answer a window over the matching resources
|
|
270
|
+
(RFC 7644 §3.4.2.4):
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
GET /scim/v2/Users?startIndex=201&count=100
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"totalResults": 4213,
|
|
279
|
+
"startIndex": 201,
|
|
280
|
+
"itemsPerPage": 100,
|
|
281
|
+
"Resources": [...]
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
- `count` is capped at `--scim-max-results` (200). A `count` of 0 is legal and
|
|
286
|
+
answers `totalResults` with no resource.
|
|
287
|
+
- `startIndex` is 1-based, and is not bounded by the page size: any offset into
|
|
288
|
+
the collection is reachable.
|
|
289
|
+
- `totalResults` is the real size of the result set, not the size of the page.
|
|
290
|
+
|
|
291
|
+
Only the requested page is held in memory, but counting the rest still means
|
|
292
|
+
walking to the end of the result set. `--scim-max-scanned` bounds that walk at
|
|
293
|
+
10000 entries; a query matching more answers `400 tooMany`, which RFC 7644
|
|
294
|
+
§3.12 defines for "a filter [that] yields many more results than the server is
|
|
295
|
+
willing to calculate or process". Raise the flag, or narrow the query.
|
|
296
|
+
|
|
297
|
+
If the directory's own size limit is lower than the collection, it refuses the
|
|
298
|
+
search first and the answer is the same `tooMany` — raise `sizelimit` in slapd.
|
|
299
|
+
|
|
250
300
|
## Multi-tenant: per-user LDAP bases
|
|
251
301
|
|
|
252
302
|
SCIM does not expose any tenant concept to clients — resource `id`s are opaque
|
|
@@ -562,6 +612,6 @@ the `configurable` role automatically, and `core/configApi` picks it up.
|
|
|
562
612
|
- No `/Me` endpoint (RFC 7644 §3.11).
|
|
563
613
|
- ETag / `If-Match` advertised but not enforced.
|
|
564
614
|
- No `/.search` POST endpoint (use GET with `?filter=…`).
|
|
565
|
-
-
|
|
566
|
-
|
|
567
|
-
|
|
615
|
+
- A list walks its result set to count it, bounded by `--scim-max-scanned`
|
|
616
|
+
(10000 by default). Past that bound it answers `400 tooMany`, as RFC 7644
|
|
617
|
+
§3.12 allows; raise the bound or narrow the query with a filter.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ldap-rest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Light directory manager",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Identity-Manager"
|
|
@@ -168,6 +168,10 @@
|
|
|
168
168
|
"types": "./dist/src/plugins/scim/groups.d.ts",
|
|
169
169
|
"import": "./dist/plugins/scim/groups.js"
|
|
170
170
|
},
|
|
171
|
+
"./plugin-scim-list": {
|
|
172
|
+
"types": "./dist/src/plugins/scim/list.d.ts",
|
|
173
|
+
"import": "./dist/plugins/scim/list.js"
|
|
174
|
+
},
|
|
171
175
|
"./plugin-scim-mapping": {
|
|
172
176
|
"types": "./dist/src/plugins/scim/mapping.d.ts",
|
|
173
177
|
"import": "./dist/plugins/scim/mapping.js"
|