ldap-rest 0.3.1 → 0.3.3

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.
@@ -1 +1 @@
1
- import{fileURLToPath as a}from"url";import{join as _,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",_(e(a(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",_(e(a(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",_(e(a(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--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",""],["--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-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-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-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{t 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"],["--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",""],["--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-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--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-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-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{t as default};
@@ -1 +1 @@
1
- let r;const setLogger=t=>{r=t},getLogger=()=>r,tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(t,e,n)=>{r?.warn(`Client error ${t}: ${n}`),e.status(t).json({error:n})},ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(t,e)=>{const n=e instanceof Error?e.message:String(e);let s=e instanceof Error&&"statusCode"in e?e.statusCode:500;if(500===s&&/\[authz-forbidden\]/.test(n)&&(s=403),s>=400&&s<500){const e=403===s&&/\[authz-forbidden\]/.test(n)?"Token does not have permission on this branch":n;return r.warn(`Client error ${s}: ${e}`),void t.status(s).json({error:e})}e instanceof Error?r.error(e.message,e):"string"==typeof e?r.error(e):r.error("Server error",e),t.status(s).json({error:"check logs"})},wantJson=(r,t)=>!1!==r.accepts("json")||(badRequest(t),!1),jsonBody=(r,t,...e)=>{try{if(!wantJson(r,t))return!1;const n=r.body;if(e.length>0)for(let r=0;r<e.length;r++)if(null==n[e[r]])return badRequest(t,"Bad content"),!1;return n}catch(r){return badRequest(t,"Bad content"),!1}};export{badRequest,created,getLogger,jsonBody,notFound,ok,serverError,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
1
+ let r;const setLogger=t=>{r=t},getLogger=()=>r,tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(t,e,n)=>{r?.warn(`Client error ${t}: ${n}`),e.status(t).json({error:n})},ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),forbidden=(r,t="Forbidden")=>_rejectResponse(403,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(t,e)=>{const n=e instanceof Error?e.message:String(e);let o=e instanceof Error&&"statusCode"in e?e.statusCode:500;if(500===o&&/\[authz-forbidden\]/.test(n)&&(o=403),o>=400&&o<500){const e=403===o&&/\[authz-forbidden\]/.test(n)?"Token does not have permission on this branch":n;return r.warn(`Client error ${o}: ${e}`),void t.status(o).json({error:e})}e instanceof Error?r.error(e.message,e):"string"==typeof e?r.error(e):r.error("Server error",e),t.status(o).json({error:"check logs"})},wantJson=(r,t)=>!1!==r.accepts("json")||(badRequest(t),!1),jsonBody=(r,t,...e)=>{try{if(!wantJson(r,t))return!1;const n=r.body;if(e.length>0)for(let r=0;r<e.length;r++)if(null==n[e[r]])return badRequest(t,"Bad content"),!1;return n}catch(r){return badRequest(t,"Bad content"),!1}};export{badRequest,created,forbidden,getLogger,jsonBody,notFound,ok,serverError,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
@@ -0,0 +1 @@
1
+ import e from"../../abstract/plugin.js";import{forbidden as t}from"../../lib/expressFormatedResponses.js";const r=/^[\w/.\-+*]*$/;function globToRegex(e){if(!r.test(e))throw new Error(`Invalid glob pattern: "${e}" — only [a-zA-Z0-9_/.-+*] are allowed`);const t=e.replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/\\\*\\\*/g,".*").replace(/\\\*/g,"[^/]*");return new RegExp(`^${t}$`)}class AuthzPerRoute extends e{constructor(...e){super(...e),this.name="authzPerRoute",this.roles=["authz"],this.rules=new Map;const t=this.config.authz_per_route??[];for(const e of t)this.parseEntry(e);const r=[...this.rules.entries()].map(([e,t])=>`${e}: ${t.some(e=>"wildcard"===e.kind)?"full access":`${t.length} rule${1!==t.length?"s":""}`}`).join(", ");this.logger.info(`authzPerRoute: ${this.rules.size} user${1!==this.rules.size?"s":""} configured${this.rules.size>0?` (${r})`:""}`)}parseEntry(e){const t=e.split(":").map(e=>e.trim());if(t.length<2)return void this.logger.warn(`authzPerRoute: ignoring invalid rule entry: ${e}`);const r=t[0];if(r)if(2!==t.length||"*"!==t[1]){if(t.length>=3){const i=t[1].toUpperCase(),o=new Set(["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS","*"]);if(!i||!o.has(i))return void this.logger.warn(`authzPerRoute: ignoring rule with invalid method "${t[1]}" in entry: ${e}`);const n=t.slice(2).join(":").trim();if(!n)return void this.logger.warn(`authzPerRoute: ignoring rule with empty path pattern in entry: ${e}`);let s;try{s=globToRegex(n)}catch{return void this.logger.warn(`authzPerRoute: ignoring entry with invalid glob "${n}" in rule: ${e}`)}return void this.addRule(r,{kind:"method-path",method:i,pathRe:s})}this.logger.warn(`authzPerRoute: ignoring invalid rule entry: ${e}`)}else this.addRule(r,{kind:"wildcard"});else this.logger.warn(`authzPerRoute: ignoring rule with empty user: ${e}`)}addRule(e,t){const r=this.rules.get(e);r?r.push(t):this.rules.set(e,[t])}matches(e,t,r){for(const i of e){if("wildcard"===i.kind)return!0;if(("*"===i.method||i.method===t.toUpperCase())&&i.pathRe.test(r))return!0}return!1}api(e){e.use((e,r,i)=>{const o=e.user;if(!o)return i();const n=this.rules.get(o);return n?this.matches(n,e.method,e.path)?i():(this.logger.warn(`authzPerRoute: user '${o}' denied for ${e.method} ${e.path}`),t(r)):(this.logger.warn(`authzPerRoute: user '${o}' denied for ${e.method} ${e.path} (no rules configured)`),t(r))})}}export{AuthzPerRoute as default,globToRegex};
@@ -1 +1 @@
1
- var e=["core/auth/trustedProxy","core/weblogs","core/auth/crowdsec","core/auth/rateLimit","core/auth/token","core/auth/llng","core/auth/openidconnect"];export{e as default};
1
+ var e=["core/auth/trustedProxy","core/weblogs","core/auth/crowdsec","core/auth/rateLimit","core/auth/token","core/auth/llng","core/auth/openidconnect","core/auth/authzPerRoute"];export{e as default};
@@ -1 +1 @@
1
- import{launchHooks as t}from"../../lib/utils.js";import{SCHEMA_BULK_RESPONSE as s}from"./types.js";import{scimInvalidValue as e,ScimError as o}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";const r=/^bulkId:([A-Za-z0-9_-]+)$/;class ScimBulk{constructor(t){this.config=t.config,this.logger=t.logger,this.users=t.users,this.groups=t.groups,this.scimPrefix=t.scimPrefix,this.hooks=t.hooks}async execute(o,r){const i=this.config.scim_bulk_max_operations||100;if(!r.Operations||!Array.isArray(r.Operations))throw e("Missing Operations array");if(r.Operations.length>i)throw e(`Too many operations: ${r.Operations.length} (max ${i})`);const a=new Map,n=[];let c=0;const u=r.failOnErrors??0;for(const t of r.Operations){if(u>0&&c>=u)break;const s=await this.executeOne(o,t,a);n.push(s),parseInt(s.status,10)>=400&&c++}const h={schemas:[s],Operations:n};return t(this.hooks.scimbulkdone,[h]),h}async executeOne(t,s,i){const a={method:s.method,bulkId:s.bulkId,status:"200"};try{if(!s.method||!s.path)throw e("method and path are required");s.data&&"object"==typeof s.data&&this.resolveRefs(s.data,i);const o=s.path.replace(/^\//,"").split("/"),n=o[0],c=o[1];if("Users"!==n&&"Groups"!==n)throw e(`Unsupported bulk path '${s.path}'`);const u="Users"===n;let h=c;if(h&&r.test(h)){const t=r.exec(h),s=i.get(t[1]);if(!s)throw e(`Unknown bulkId reference '${t[1]}'`);h=s.id}switch(s.method){case"POST":{if(h)throw e("POST must not include an id");const o=u?await this.users.create(t,s.data):await this.groups.create(t,s.data);return a.status="201",a.location=this.buildLocation(n,o.id,t),a.version=o.meta?.version||void 0,s.bulkId&&i.set(s.bulkId,{id:o.id,type:u?"User":"Group"}),a}case"PUT":{if(!h)throw e("PUT requires an id");const o=u?await this.users.replace(t,h,s.data):await this.groups.replace(t,h,s.data);return a.status="200",a.location=this.buildLocation(n,o.id,t),a}case"PATCH":{if(!h)throw e("PATCH requires an id");const o=u?await this.users.patch(t,h,s.data):await this.groups.patch(t,h,s.data);return a.status="200",a.location=this.buildLocation(n,o.id,t),a}case"DELETE":if(!h)throw e("DELETE requires an id");return u?await this.users.delete(t,h):await this.groups.delete(t,h),a.status="204",a;default:throw e(`Unsupported method '${String(s.method)}'`)}}catch(t){const s=t instanceof o?t.statusCode:500;return a.status=String(s),a.response={schemas:["urn:ietf:params:scim:api:messages:2.0:Error"],status:String(s),...t instanceof o&&t.scimType?{scimType:t.scimType}:{},detail:t instanceof Error?t.message:String(t)},a}}resolveRefs(t,s){for(const[e,o]of Object.entries(t)){if("string"==typeof o){const i=r.exec(o);if(i){const o=s.get(i[1]);o&&(t[e]=o.id)}continue}if(Array.isArray(o)){const t=o;for(let e=0;e<t.length;e++){const o=t[e];if("string"==typeof o){const i=r.exec(o);if(i){const o=s.get(i[1]);o&&(t[e]=o.id)}}else o&&"object"==typeof o&&this.resolveRefs(o,s)}continue}o&&"object"==typeof o&&this.resolveRefs(o,s)}}buildLocation(t,s,e){const o=this.config.scim_base_url||"";return`${o?o.replace(/\/$/,""):e?.protocol&&e.get?`${e.protocol}://${String(e.get("host")||"")}`:""}${this.scimPrefix}/${t}/${encodeURIComponent(s)}`}}export{ScimBulk};
1
+ import{launchHooks as t}from"../../lib/utils.js";import{SCHEMA_BULK_RESPONSE as s}from"./types.js";import{scimInvalidValue as e,ScimError as o}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";const r=/^bulkId:([A-Za-z0-9_-]+)$/;class ScimBulk{constructor(t){this.config=t.config,this.logger=t.logger,this.users=t.users,this.groups=t.groups,this.scimPrefix=t.scimPrefix,this.hooks=t.hooks}async execute(o,r){const i=this.config.scim_bulk_max_operations||100;if(!r.Operations||!Array.isArray(r.Operations))throw e("Missing Operations array");if(r.Operations.length>i)throw e(`Too many operations: ${r.Operations.length} (max ${i})`);const a=new Map,n=[];let c=0;const u=r.failOnErrors??0;for(const t of r.Operations){if(u>0&&c>=u)break;const s=await this.executeOne(o,t,a);n.push(s),parseInt(s.status,10)>=400&&c++}const h={schemas:[s],Operations:n};return t(this.hooks.scimbulkdone,h),h}async executeOne(t,s,i){const a={method:s.method,bulkId:s.bulkId,status:"200"};try{if(!s.method||!s.path)throw e("method and path are required");s.data&&"object"==typeof s.data&&this.resolveRefs(s.data,i);const o=s.path.replace(/^\//,"").split("/"),n=o[0],c=o[1];if("Users"!==n&&"Groups"!==n)throw e(`Unsupported bulk path '${s.path}'`);const u="Users"===n;let h=c;if(h&&r.test(h)){const t=r.exec(h),s=i.get(t[1]);if(!s)throw e(`Unknown bulkId reference '${t[1]}'`);h=s.id}switch(s.method){case"POST":{if(h)throw e("POST must not include an id");const o=u?await this.users.create(t,s.data):await this.groups.create(t,s.data);return a.status="201",a.location=this.buildLocation(n,o.id,t),a.version=o.meta?.version||void 0,s.bulkId&&i.set(s.bulkId,{id:o.id,type:u?"User":"Group"}),a}case"PUT":{if(!h)throw e("PUT requires an id");const o=u?await this.users.replace(t,h,s.data):await this.groups.replace(t,h,s.data);return a.status="200",a.location=this.buildLocation(n,o.id,t),a}case"PATCH":{if(!h)throw e("PATCH requires an id");const o=u?await this.users.patch(t,h,s.data):await this.groups.patch(t,h,s.data);return a.status="200",a.location=this.buildLocation(n,o.id,t),a}case"DELETE":if(!h)throw e("DELETE requires an id");return u?await this.users.delete(t,h):await this.groups.delete(t,h),a.status="204",a;default:throw e(`Unsupported method '${String(s.method)}'`)}}catch(t){const s=t instanceof o?t.statusCode:500;return a.status=String(s),a.response={schemas:["urn:ietf:params:scim:api:messages:2.0:Error"],status:String(s),...t instanceof o&&t.scimType?{scimType:t.scimType}:{},detail:t instanceof Error?t.message:String(t)},a}}resolveRefs(t,s){for(const[e,o]of Object.entries(t)){if("string"==typeof o){const i=r.exec(o);if(i){const o=s.get(i[1]);o&&(t[e]=o.id)}continue}if(Array.isArray(o)){const t=o;for(let e=0;e<t.length;e++){const o=t[e];if("string"==typeof o){const i=r.exec(o);if(i){const o=s.get(i[1]);o&&(t[e]=o.id)}}else o&&"object"==typeof o&&this.resolveRefs(o,s)}continue}o&&"object"==typeof o&&this.resolveRefs(o,s)}}buildLocation(t,s,e){const o=this.config.scim_base_url||"";return`${o?o.replace(/\/$/,""):e?.protocol&&e.get?`${e.protocol}://${String(e.get("host")||"")}`:""}${this.scimPrefix}/${t}/${encodeURIComponent(s)}`}}export{ScimBulk};
@@ -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 l}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{patchToModifyRequest as d}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany 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.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 l={value:h};if(m&&(l.type=m,i)){const t="User"===m?"Users":"Groups";l.$ref=`${i.replace(/\/$/,"")}${r}/${t}/${encodeURIComponent(h)}`}return l}}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)}catch(t){if(32===g(t))throw f(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw f(`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=p(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 b&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const c=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:[...u(this.mapping),"member"],sizeLimit:this.maxResults+1},s)).searchEntries||[],h=c.length;if(h>this.maxResults)throw y(`Filter matched ${h} resources, exceeds max ${this.maxResults}`);const l=c.slice(r-1,r-1+i),d=this.buildMemberResolver(t),g=l.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}=l(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)}catch(t){if(68===g(t))throw $(`Group ${n} already exists`);throw t}const p=await this.get(i,n);return r(this.hooks.scimgroupcreatedone,[p]),p}async replace(t,s,i){await this.get(t,s);const a=(await e(this.hooks.scimgroupupdate,[s,i,t]))[1],{attributes:o}=l(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);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 d(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 t=await this.ldap.search({paged:!1,scope:"base",attributes:["member"]},o),e=t.searchEntries[0]?.member,s=Array.isArray(e)?e:e?[String(e)]:[],r=Array.isArray(i.delete.member)?i.delete.member:[String(i.delete.member)];0===s.filter(t=>!r.includes(t)).length&&(i.add||(i.add={}),i.add.member=[a])}await this.ldap.modify(o,i);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),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);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}}get groupMapping(){return this.mapping}}export{ScimGroups};
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 l}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{patchToModifyRequest as d}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany 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.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 l={value:h};if(m&&(l.type=m,i)){const t="User"===m?"Users":"Groups";l.$ref=`${i.replace(/\/$/,"")}${r}/${t}/${encodeURIComponent(h)}`}return l}}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)}catch(t){if(32===g(t))throw f(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw f(`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=p(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 b&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const c=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:[...u(this.mapping),"member"],sizeLimit:this.maxResults+1},s)).searchEntries||[],h=c.length;if(h>this.maxResults)throw y(`Filter matched ${h} resources, exceeds max ${this.maxResults}`);const l=c.slice(r-1,r-1+i),d=this.buildMemberResolver(t),g=l.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}=l(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)}catch(t){if(68===g(t))throw $(`Group ${n} already exists`);throw t}const p=await this.get(i,n);return r(this.hooks.scimgroupcreatedone,p),p}async replace(t,s,i){await this.get(t,s);const a=(await e(this.hooks.scimgroupupdate,[s,i,t]))[1],{attributes:o}=l(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);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 d(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 t=await this.ldap.search({paged:!1,scope:"base",attributes:["member"]},o),e=t.searchEntries[0]?.member,s=Array.isArray(e)?e:e?[String(e)]:[],r=Array.isArray(i.delete.member)?i.delete.member:[String(i.delete.member)];0===s.filter(t=>!r.includes(t)).length&&(i.add||(i.add={}),i.add.member=[a])}await this.ldap.modify(o,i);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),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);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}}get groupMapping(){return this.mapping}}export{ScimGroups};
@@ -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 h}from"./types.js";import{mergeMapping as n,loadMappingFile as o,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{patchToModifyRequest as m}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany 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.scimPrefix=this.config.scim_prefix||"/scim/v2";const s=this.config.scim_user_mapping||"";this.mapping=n(c,s?o(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)}catch(t){if(32===g(t))throw f(`User ${s} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw f(`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,n=`(${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:n=`(&${n}${t.ldapFilter})`}if(a)try{const s=await this.get(t,a);return{schemas:[h],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[s]}}catch(t){if(t instanceof b&&404===t.statusCode)return{schemas:[h],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const o=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:l(this.mapping),sizeLimit:this.maxResults+1},e)).searchEntries||[],c=o.length;if(c>this.maxResults)throw w(`Filter matched ${c} resources, exceeds max ${this.maxResults}`);const u=o.slice(i-1,i-1+r);s.sortBy,s.sortOrder;const m=u.map(s=>d(s,this.mapping,this.ctx(t)));return{schemas:[h],totalResults:c,startIndex:i,itemsPerPage:m.length,Resources:m}}async create(r,a){if(!a.userName)throw y("userName is required");const h=(await s(this.hooks.scimusercreate,[a,r]))[0],{rdn:n,attributes:o}=u(h,this.mapping,this.ctx(r),this.objectClass);if(!n)throw y("userName is required");e(n,this.rdnAttribute),o[this.rdnAttribute]=n;const c=this.baseResolver.userBase(r),l=`${this.rdnAttribute}=${t(n)},${c}`;try{await this.ldap.add(l,o)}catch(t){if(68===g(t))throw x(`User ${n} already exists`);throw t}const d=await this.get(r,n);return i(this.hooks.scimusercreatedone,[d]),d}async replace(t,e,r){const a=this.dnForId(e,t),h=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},a);if(!h.searchEntries?.length)throw f(`User ${e} not found`);const n=h.searchEntries[0],o=(await s(this.hooks.scimuserupdate,[e,r,t]))[1],{attributes:c}=u(o,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(n[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)}catch(t){if(16!==g(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 m(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);const h=await this.get(t,s);return i(this.hooks.scimuserupdatedone,[s,h]),h}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),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 h=`${this.rdnAttribute}=${t(e)},${i}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},h);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.search({filter:`(${this.rdnAttribute}=${a(e)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}}get userMapping(){return this.mapping}}export{ScimUsers};
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 h}from"./types.js";import{mergeMapping as n,loadMappingFile as o,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{patchToModifyRequest as m}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany 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.scimPrefix=this.config.scim_prefix||"/scim/v2";const s=this.config.scim_user_mapping||"";this.mapping=n(c,s?o(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)}catch(t){if(32===g(t))throw f(`User ${s} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw f(`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,n=`(${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:n=`(&${n}${t.ldapFilter})`}if(a)try{const s=await this.get(t,a);return{schemas:[h],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[s]}}catch(t){if(t instanceof b&&404===t.statusCode)return{schemas:[h],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const o=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:l(this.mapping),sizeLimit:this.maxResults+1},e)).searchEntries||[],c=o.length;if(c>this.maxResults)throw w(`Filter matched ${c} resources, exceeds max ${this.maxResults}`);const u=o.slice(i-1,i-1+r);s.sortBy,s.sortOrder;const m=u.map(s=>d(s,this.mapping,this.ctx(t)));return{schemas:[h],totalResults:c,startIndex:i,itemsPerPage:m.length,Resources:m}}async create(r,a){if(!a.userName)throw y("userName is required");const h=(await s(this.hooks.scimusercreate,[a,r]))[0],{rdn:n,attributes:o}=u(h,this.mapping,this.ctx(r),this.objectClass);if(!n)throw y("userName is required");e(n,this.rdnAttribute),o[this.rdnAttribute]=n;const c=this.baseResolver.userBase(r),l=`${this.rdnAttribute}=${t(n)},${c}`;try{await this.ldap.add(l,o)}catch(t){if(68===g(t))throw x(`User ${n} already exists`);throw t}const d=await this.get(r,n);return i(this.hooks.scimusercreatedone,d),d}async replace(t,e,r){const a=this.dnForId(e,t),h=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},a);if(!h.searchEntries?.length)throw f(`User ${e} not found`);const n=h.searchEntries[0],o=(await s(this.hooks.scimuserupdate,[e,r,t]))[1],{attributes:c}=u(o,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(n[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)}catch(t){if(16!==g(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 m(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);const h=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,h),h}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),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 h=`${this.rdnAttribute}=${t(e)},${i}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},h);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.search({filter:`(${this.rdnAttribute}=${a(e)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}}get userMapping(){return this.mapping}}export{ScimUsers};
@@ -0,0 +1 @@
1
+ import{Buffer as t}from"buffer";import e from"node-fetch";import i from"../../abstract/plugin.js";class CozyProvision extends i{constructor(t){super(t),this.name="cozyProvision",this.roles=["consistency"],this.publisher=null,this.publisherInit=null,this.hooks={scimusercreatedone:async t=>{await this.createCozyInstance(t)&&await this.publishUserCreated(t)},scimuserdeletedone:async t=>{await this.publishUserDeleted(t)}},this.cozyAdminUrl=(this.config.cozy_admin_url||"").replace(/\/$/,""),this.cozyAdminUser=this.config.cozy_admin_user||"admin",this.cozyAdminPassphrase=this.config.cozy_admin_passphrase||"",this.cozyOrgId=this.config.cozy_org_id||"",this.cozyOrgDomain=this.config.cozy_org_domain||"",this.cozyDefaultLocale=this.config.cozy_default_locale||"fr",this.rabbitmqUrl=this.config.rabbitmq_url||"",this.authExchange=this.config.cozy_auth_exchange||"auth",this.b2bExchange=this.config.cozy_b2b_exchange||"b2b",this.cozyAdminUrl||this.logger.warn(`${this.name}: cozy_admin_url is empty — Cozy instance creation will be skipped`),this.cozyOrgDomain||this.logger.warn(`${this.name}: cozy_org_domain is empty — workplaceFqdn cannot be composed`),this.rabbitmqUrl||this.logger.warn(`${this.name}: rabbitmq_url is empty — AMQP publishes will be skipped`),this.registerShutdown()}async createCozyInstance(i){if(!this.cozyAdminUrl)return!1;const r=this.extractId(i);if(!r)return this.logger.warn({plugin:this.name,event:"createCozyInstance",message:"user has no userName/id — skipping"}),!1;if(!this.cozyOrgDomain)return this.logger.error({plugin:this.name,event:"createCozyInstance",id:r,message:"cozy_org_domain not configured — cannot compose Domain"}),!1;const s=`${r}.${this.cozyOrgDomain}`,n=new URLSearchParams;n.set("Domain",s),n.set("Locale",this.extractLocale(i));const a=this.extractPrimaryEmail(i);a&&n.set("Email",a),this.cozyOrgId&&n.set("OrgID",this.cozyOrgId),this.cozyOrgDomain&&n.set("OrgDomain",this.cozyOrgDomain);const o=`${this.cozyAdminUrl}/instances?${n.toString()}`,c=t.from(`${this.cozyAdminUser}:${this.cozyAdminPassphrase}`).toString("base64"),h={plugin:this.name,event:"createCozyInstance",id:r,domain:s,email:a||void 0};try{const t=await e(o,{method:"POST",headers:{Authorization:`Basic ${c}`,Accept:"application/json"}});return t.ok?(this.logger.info({...h,result:"success",http_status:t.status}),!0):409===t.status?(this.logger.info({...h,result:"already_exists",http_status:t.status}),!0):(this.logger.error({...h,result:"error",http_status:t.status,http_status_text:t.statusText}),!1)}catch(t){return this.logger.error({...h,result:"error",error:`${t}`}),!1}}async publishUserCreated(t){const e=await this.getPublisher();if(!e)return;const i=this.extractId(t);if(!i)return;const r=this.extractPrimaryEmail(t),s={twakeId:i,organizationDomain:this.cozyOrgDomain,workplaceFqdn:`${i}.${this.cozyOrgDomain}`};r&&(s.email=r);try{await e.publish(this.authExchange,"user.created",s),this.logger.info({plugin:this.name,event:"publishUserCreated",result:"success",exchange:this.authExchange,routingKey:"user.created",twakeId:i})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:i,error:`${t}`})}}async publishUserDeleted(t){const e=await this.getPublisher();if(!e)return;if(!this.cozyOrgDomain)return void this.logger.error({plugin:this.name,event:"publishUserDeleted",id:t,message:"cozy_org_domain not configured — cannot compose workplaceFqdn"});const i={workplaceFqdn:`${t}.${this.cozyOrgDomain}`,domain:this.cozyOrgDomain};try{await e.publish(this.b2bExchange,"domain.user.deleted",i),this.logger.info({plugin:this.name,event:"publishUserDeleted",result:"success",exchange:this.b2bExchange,routingKey:"domain.user.deleted",workplaceFqdn:i.workplaceFqdn})}catch(e){this.logger.error({plugin:this.name,event:"publishUserDeleted",id:t,error:`${e}`})}}async getPublisher(){return this.publisher?this.publisher:this.rabbitmqUrl?(this.publisherInit||(this.publisherInit=(async()=>{try{const t=new((await import("@linagora/rabbitmq-client")).RabbitMQClient)({url:this.rabbitmqUrl});return await t.init(),this.publisher=t,this.logger.info(`${this.name}: connected to RabbitMQ at ${function(t){try{const e=new URL(t);return e.username="",e.password="",e.toString()}catch{return"amqp://[broker]"}}(this.rabbitmqUrl)}`),t}catch(t){return this.logger.warn({plugin:this.name,event:"rabbitmq_init",message:"@linagora/rabbitmq-client unavailable or broker unreachable — AMQP publishes disabled",error:`${t}`}),null}})()),this.publisherInit):null}extractId(t){return"string"==typeof t.userName&&t.userName.length>0?t.userName:"string"==typeof t.id&&t.id.length>0?t.id:null}extractPrimaryEmail(t){const e=t.emails;if(!e||0===e.length)return null;const i=(e.find(t=>t.primary)||e[0]).value;return"string"==typeof i&&i.length>0?i:null}extractLocale(t){return"string"==typeof t.locale&&t.locale.length>0?t.locale:"string"==typeof t.preferredLanguage&&t.preferredLanguage.length>0?t.preferredLanguage:this.cozyDefaultLocale}registerShutdown(){!function(t){if(r.push(t),s)return;s=!0;const fanOut=()=>{for(const t of r.splice(0))try{t()}catch{}};process.once("SIGTERM",fanOut),process.once("SIGINT",fanOut)}(()=>{const t=this.publisher;this.publisher=null,this.publisherInit=null,t&&t.close().catch(t=>{this.logger.warn({plugin:this.name,event:"shutdown",error:`${t}`})})})}}const r=[];let s=!1;export{CozyProvision as default};
@@ -76,6 +76,7 @@ export interface Config {
76
76
  oidc_client_id?: string;
77
77
  oidc_client_secret?: string;
78
78
  base_url?: string;
79
+ authz_per_route?: string[];
79
80
  authz_per_branch_config?: AuthConfig;
80
81
  authz_per_branch_cache_ttl?: number;
81
82
  authz_dynamic_base?: string;
@@ -119,6 +120,15 @@ export interface Config {
119
120
  calendar_resource_objectclass?: string;
120
121
  calendar_resource_creator?: string;
121
122
  calendar_resource_domain?: string;
123
+ cozy_admin_url?: string;
124
+ cozy_admin_user?: string;
125
+ cozy_admin_passphrase?: string;
126
+ cozy_org_id?: string;
127
+ cozy_org_domain?: string;
128
+ cozy_default_locale?: string;
129
+ cozy_auth_exchange?: string;
130
+ cozy_b2b_exchange?: string;
131
+ rabbitmq_url?: string;
122
132
  applicative_account_base?: string;
123
133
  max_app_accounts?: number;
124
134
  ldap_operational_attributes?: string[];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @module core/auth/authzPerRoute
3
+ * Route-level authorization plugin.
4
+ *
5
+ * Restricts access by HTTP method + path based on the authenticated user name
6
+ * (req.user, set by an auth plugin loaded before this one).
7
+ *
8
+ * @author Xavier Guimard <xguimard@linagora.com>
9
+ */
10
+ import type { Express } from 'express';
11
+ import DmPlugin, { type Role } from '../../abstract/plugin';
12
+ export declare function globToRegex(glob: string): RegExp;
13
+ export default class AuthzPerRoute extends DmPlugin {
14
+ name: string;
15
+ roles: Role[];
16
+ private rules;
17
+ constructor(...args: ConstructorParameters<typeof DmPlugin>);
18
+ private parseEntry;
19
+ private addRule;
20
+ private matches;
21
+ api(app: Express): void;
22
+ }
@@ -0,0 +1,56 @@
1
+ import DmPlugin, { type Role } from '../../abstract/plugin';
2
+ import type { DM } from '../../bin';
3
+ import { Hooks } from '../../hooks';
4
+ interface RabbitPublisher {
5
+ init(): Promise<void>;
6
+ publish(exchange: string, routingKey: string, message: Record<string, unknown>): Promise<void>;
7
+ close(clearSubscriptions?: boolean): Promise<void>;
8
+ }
9
+ export default class CozyProvision extends DmPlugin {
10
+ name: string;
11
+ roles: Role[];
12
+ private readonly cozyAdminUrl;
13
+ private readonly cozyAdminPassphrase;
14
+ private readonly cozyAdminUser;
15
+ private readonly cozyOrgId;
16
+ private readonly cozyOrgDomain;
17
+ private readonly cozyDefaultLocale;
18
+ private readonly rabbitmqUrl;
19
+ private readonly authExchange;
20
+ private readonly b2bExchange;
21
+ private publisher;
22
+ private publisherInit;
23
+ constructor(server: DM);
24
+ hooks: Hooks;
25
+ /**
26
+ * POST /instances on the cozy-stack admin API. Returns true on success
27
+ * (including 409 idempotent "already exists").
28
+ */
29
+ private createCozyInstance;
30
+ /**
31
+ * Publish user.created on the `auth` topic exchange. Payload matches what
32
+ * cozy-stack's stack.user.created consumer expects: a `twakeId` identifier,
33
+ * an optional email, and the organizationDomain so the consumer can match
34
+ * the instance. Sending `sub` instead causes cozy-stack to nack the message
35
+ * with "missing twakeId".
36
+ */
37
+ private publishUserCreated;
38
+ /**
39
+ * Publish domain.user.deleted on the `b2b` topic exchange so peer cozy
40
+ * instances can drop the deleted user's contact card.
41
+ */
42
+ private publishUserDeleted;
43
+ /**
44
+ * Lazy-init the AMQP publisher. The `@linagora/rabbitmq-client` dependency
45
+ * is optional — if the package or the broker is unreachable, return null
46
+ * so callers can no-op cleanly.
47
+ *
48
+ * Override this in tests to inject a stub publisher.
49
+ */
50
+ protected getPublisher(): Promise<RabbitPublisher | null>;
51
+ private extractId;
52
+ private extractPrimaryEmail;
53
+ private extractLocale;
54
+ private registerShutdown;
55
+ }
56
+ export {};
@@ -27,6 +27,7 @@ All LDAP-Rest configuration options.
27
27
  - [core/auth/openidconnect](#coreauthopenidconnect)
28
28
  - [Authorization Plugins](#authorization-plugins)
29
29
  - [core/auth/authzPerBranch](#coreauthauthzperbranch)
30
+ - [core/auth/authzPerRoute](#coreauthauthzperroute)
30
31
  - [core/auth/authzLinid1](#coreauthauthzlinid1)
31
32
  - [Security Plugins](#security-plugins)
32
33
  - [core/auth/rateLimit](#coreauthratelimit)
@@ -236,6 +237,12 @@ Maintains consistency of department links when organizations are renamed/moved.
236
237
  | `--authz-per-branch-config` | `DM_AUTHZ_PER_BRANCH_CONFIG` | `{default:{read:true,write:false,delete:false}}` | Authorization config (JSON) |
237
238
  | `--authz-per-branch-cache-ttl` | `DM_AUTHZ_PER_BRANCH_CACHE_TTL` | `60` | Cache TTL (seconds) |
238
239
 
240
+ #### `core/auth/authzPerRoute`
241
+
242
+ | CLI | Env | Default | Description |
243
+ | ------------------- | --------------------- | ------- | ---------------------------------------------------------------------- |
244
+ | `--authz-per-route` | `DM_AUTHZ_PER_ROUTES` | `[]` | Per-user route ACL rules (see [docs](plugins/auth/authz-per-route.md)) |
245
+
239
246
  #### `core/auth/authzLinid1`
240
247
 
241
248
  | CLI | Env | Default | Description |
@@ -17,6 +17,7 @@ LDAP-Rest provides multiple authentication plugins to secure API access. These p
17
17
  | Method | Plugin | Description |
18
18
  | ----------------------------------------------- | -------------------------- | --------------------------- |
19
19
  | [Authorization Per Branch](authz-per-branch.md) | `core/auth/authzPerBranch` | Branch-level access control |
20
+ | [Authorization Per Route](authz-per-route.md) | `core/auth/authzPerRoute` | HTTP route-level ACL |
20
21
  | [Authorization LinID 1.x](authz-linid1.md) | `core/auth/authzLinid1` | LinID 1.x integration |
21
22
 
22
23
  ## Security Plugins
@@ -0,0 +1,144 @@
1
+ # Route-Level Authorization (`authzPerRoute`)
2
+
3
+ Restricts access to specific HTTP routes based on the authenticated user's name
4
+ (`req.user`), which is set by an auth plugin such as `core/auth/token`.
5
+
6
+ This plugin complements `authzPerBranch` (LDAP-branch ACL) and `authzDynamic`
7
+ (LDAP-backed tokens): whereas those operate at the LDAP-branch level,
8
+ `authzPerRoute` works at the HTTP route level and needs no LDAP connection.
9
+
10
+ ## Plugin loading order
11
+
12
+ `authzPerRoute` MUST run **after** the authentication plugin (so `req.user` is set)
13
+ AND **before** any API route plugin (so its Express middleware is registered before
14
+ the routes it protects).
15
+
16
+ When loaded via the CLI plugin loader, plugins not listed in
17
+ `src/plugins/priority.json` are loaded in parallel — this can race with API route
18
+ registration and result in an authorization bypass. To guarantee correct ordering,
19
+ `core/auth/authzPerRoute` is included in the priority list and loads sequentially
20
+ after the other auth plugins. If you build a custom loader, ensure equivalent
21
+ sequencing.
22
+
23
+ ```bash
24
+ --plugin core/auth/token \
25
+ --plugin core/auth/authzPerRoute
26
+ ```
27
+
28
+ ## Configuration
29
+
30
+ ### CLI
31
+
32
+ ```bash
33
+ --authz-per-route "full-access:*" \
34
+ --authz-per-route "updt-only:POST:/api/v1/ldap/updt" \
35
+ --authz-per-route "updt-only:GET:/api/v1/ldap/updt/**"
36
+ ```
37
+
38
+ ### Environment Variable
39
+
40
+ ```bash
41
+ DM_AUTHZ_PER_ROUTES="full-access:*,updt-only:POST:/api/v1/ldap/updt,updt-only:GET:/api/v1/ldap/updt/**"
42
+ ```
43
+
44
+ Multiple values are comma-separated. Repeating `--authz-per-route` on the CLI
45
+ is equivalent.
46
+
47
+ ## Rule Syntax
48
+
49
+ Each entry has one of two forms:
50
+
51
+ | Form | Meaning |
52
+ | ---------------------------- | ----------------------------------------------------------- |
53
+ | `<user>:*` | Full wildcard — allow any method on any path |
54
+ | `<user>:<METHOD>:<pathGlob>` | Allow only the given HTTP method on paths matching the glob |
55
+
56
+ - `<user>` — the value of `req.user` as set by the auth plugin.
57
+ - `<METHOD>` — an HTTP verb (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`,
58
+ `OPTIONS`) or `*` (any verb).
59
+ - `<pathGlob>` — a glob pattern (not a regex). Patterns are implicitly
60
+ anchored start-to-end. Rules:
61
+ - `*` matches any sequence of characters **except** `/` (one path segment).
62
+ - `**` matches any sequence of characters **including** `/` (multiple segments).
63
+ - All other characters are matched literally — regex special characters (`.`, `+`,
64
+ `?`, etc.) have no special meaning.
65
+ - **Allowed characters:** alphanumerics (`a-z`, `A-Z`, `0-9`), `/`, `_`, `-`,
66
+ `.`, `+`, and `*`. Globs containing any other character (e.g. `;`, space,
67
+ `?`) are rejected at startup with a warning and the rule is skipped.
68
+
69
+ A user may have multiple entries; access is granted if **any** rule matches
70
+ (logical OR).
71
+
72
+ ### Examples
73
+
74
+ ```bash
75
+ # admin token: full access
76
+ full-access:*
77
+
78
+ # read-only token: GET only on exactly /api/hello
79
+ reader:GET:/api/hello
80
+
81
+ # update token: POST on exact path, GET on that path and all sub-paths
82
+ updt:POST:/api/v1/ldap/updt
83
+ updt:GET:/api/v1/ldap/updt**
84
+
85
+ # any method on a path and all sub-paths
86
+ any-method:*:/api/v1/ldap/users**
87
+
88
+ # match one level deep only (not /api/v1/ldap/users/uid=bob/sub)
89
+ one-level:GET:/api/v1/ldap/users/*
90
+ ```
91
+
92
+ ### Glob pattern quick reference
93
+
94
+ | Pattern | Matches | Does NOT match |
95
+ | ---------------- | ----------------------------------------------------- | ----------------------------------- |
96
+ | `/api/hello` | `/api/hello` | `/api/hello/sub` |
97
+ | `/api/hello**` | `/api/hello`, `/api/hello/sub`, `/api/hello/sub/deep` | `/api/hell` |
98
+ | `/api/hello/**` | `/api/hello/sub`, `/api/hello/sub/deep` | `/api/hello` |
99
+ | `/api/hello/*` | `/api/hello/sub` | `/api/hello`, `/api/hello/sub/deep` |
100
+ | `/api/hello.bak` | `/api/hello.bak` | `/api/helloXbak` (`.` is literal) |
101
+
102
+ ## Behavior
103
+
104
+ | Situation | Result |
105
+ | ----------------------------------------------------- | ------------------------------------------------------ |
106
+ | `req.user` is unset (no auth plugin ran) | Pass through (`next()`) — let upstream auth return 401 |
107
+ | User authenticated but has no rules configured | 403 Forbidden |
108
+ | User has at least one matching rule | 200 / pass through |
109
+ | User has rules but none match the current method+path | 403 Forbidden |
110
+
111
+ ## Full Example
112
+
113
+ ```bash
114
+ DM_AUTH_TOKENS="tok-admin:admin,tok-ro:reader"
115
+ DM_AUTHZ_PER_ROUTES="admin:*,reader:GET:/api/v1/ldap/users**,reader:GET:/api/v1/ldap/groups**"
116
+
117
+ npx ldap-rest \
118
+ --plugin core/auth/token \
119
+ --plugin core/auth/authzPerRoute \
120
+ --plugin core/ldap/users \
121
+ --plugin core/ldap/groups \
122
+ --ldap-url ldap://localhost:389 \
123
+ ...
124
+ ```
125
+
126
+ ```bash
127
+ # admin: any request allowed
128
+ curl -H "Authorization: Bearer tok-admin" http://api/v1/ldap/users
129
+ curl -H "Authorization: Bearer tok-admin" -X DELETE http://api/v1/ldap/users/uid=bob,ou=users,dc=example,dc=org
130
+
131
+ # reader: GET allowed
132
+ curl -H "Authorization: Bearer tok-ro" http://api/v1/ldap/users # 200
133
+
134
+ # reader: write denied
135
+ curl -H "Authorization: Bearer tok-ro" -X POST http://api/v1/ldap/users # 403
136
+ ```
137
+
138
+ ## Difference vs. Other Authz Plugins
139
+
140
+ | Plugin | Scope | Requires LDAP |
141
+ | ---------------- | ------------------------------ | ------------------ |
142
+ | `authzPerRoute` | HTTP method + path | No |
143
+ | `authzPerBranch` | LDAP branch read/write/delete | No (static config) |
144
+ | `authzDynamic` | LDAP branch ACL stored in LDAP | Yes |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"
@@ -52,6 +52,10 @@
52
52
  "types": "./dist/src/plugins/auth/authzPerBranch.d.ts",
53
53
  "import": "./dist/plugins/auth/authzPerBranch.js"
54
54
  },
55
+ "./plugin-auth-authzperroute": {
56
+ "types": "./dist/src/plugins/auth/authzPerRoute.d.ts",
57
+ "import": "./dist/plugins/auth/authzPerRoute.js"
58
+ },
55
59
  "./plugin-auth-crowdsec": {
56
60
  "types": "./dist/src/plugins/auth/crowdsec.d.ts",
57
61
  "import": "./dist/plugins/auth/crowdsec.js"
@@ -192,6 +196,10 @@
192
196
  "types": "./dist/src/plugins/twake/calendarResources.d.ts",
193
197
  "import": "./dist/plugins/twake/calendarResources.js"
194
198
  },
199
+ "./plugin-twake-cozyprovision": {
200
+ "types": "./dist/src/plugins/twake/cozyProvision.d.ts",
201
+ "import": "./dist/plugins/twake/cozyProvision.js"
202
+ },
195
203
  "./plugin-twake-drive": {
196
204
  "types": "./dist/src/plugins/twake/drive.d.ts",
197
205
  "import": "./dist/plugins/twake/drive.js"
@@ -631,6 +639,7 @@
631
639
  "typescript": "^5.9.2"
632
640
  },
633
641
  "optionalDependencies": {
642
+ "@linagora/rabbitmq-client": "^0.1.2",
634
643
  "express-openid-connect": "^2.19.2",
635
644
  "lemonldap-ng-handler": "^0.7.3",
636
645
  "re2": "^1.22.1"