diff --git a/README.md b/README.md index af95112..4c1aa8c 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,61 @@ ENABLE_NOTIFICATION=true NOTIFICATION_URL=https://your-notification-service.com ``` +### 🔑 SSH Public Key Support (openssh-lpk) + +The LDAP gateway supports SSH public key storage and retrieval, compatible with the openssh-lpk schema used by SSSD and other LDAP-aware SSH implementations. + +#### Database Schema + +Add an `sshpublickey` column to your users table: + +```sql +-- MySQL/MariaDB/PostgreSQL +ALTER TABLE users ADD COLUMN sshpublickey TEXT; + +-- Store SSH public keys (supports multiple keys) +UPDATE users SET sshpublickey = 'ssh-rsa AAAAB3NzaC1yc2EAAA... user@host' WHERE username = 'alice'; +``` + +#### SQL Query Configuration + +Update your `SQL_QUERY_ONE_USER` and `SQL_QUERY_ALL_USERS` to include the `sshpublickey` column: + +```ini +SQL_QUERY_ONE_USER='SELECT username, uid_number, gid_number, full_name, mail, home_directory, login_shell, sshpublickey FROM users WHERE username = ?' +SQL_QUERY_ALL_USERS='SELECT username, uid_number, gid_number, full_name, mail, home_directory, login_shell, sshpublickey FROM users' +``` + +#### LDAP Attributes + +When SSH public keys are present, the LDAP gateway automatically: +- Adds the `ldapPublicKey` objectClass to user entries +- Includes the `sshPublicKey` attribute with the key(s) +- Supports multiple SSH keys per user (stored as multi-value LDAP attribute) + +#### SSSD Configuration + +Configure SSSD to use SSH public keys from LDAP: + +```ini +# /etc/sssd/sssd.conf +[domain/ldap] +ldap_user_ssh_public_key = sshPublicKey + +[ssh] +ssh_key_cache_timeout = 300 +``` + +Update SSHD configuration: + +```bash +# /etc/ssh/sshd_config +AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys +AuthorizedKeysCommandUser nobody +``` + +Now SSH authentication will retrieve public keys from LDAP automatically! + ### 🔌 Custom Backends (Dynamic Loading) **NEW:** Create your own backends without rebuilding! Place JavaScript files in `server/backends/` to add custom authentication or directory providers. @@ -523,6 +578,7 @@ Direct integration with Proxmox virtualization environments: - **Container Authentication** → Centralized LDAP for all containers/VMs - **Configuration Syncing** → Reads directly from Proxmox user/shadow files - **MFA Support** → Optional push notifications via [MIE Authenticator](https://github.com/mieweb/mieweb_auth_app) +- **SSH Public Key Support** → Compatible with openssh-lpk schema for SSH key authentication via SSSD - **Automated Setup** → Use [pown.sh](https://github.com/mieweb/pown.sh) for container LDAP client configuration ### Deployment diff --git a/npm/src/LdapEngine.js b/npm/src/LdapEngine.js index 4d303e5..cb2b887 100644 --- a/npm/src/LdapEngine.js +++ b/npm/src/LdapEngine.js @@ -264,6 +264,13 @@ class LdapEngine extends EventEmitter { let entryCount = 0; const startTime = Date.now(); + // ldapjs compares lowercased entry attribute names against res.attributes, + // but does not lowercase res.attributes itself. Normalize to lowercase so + // that case-insensitive attribute requests (e.g. sshPublicKey) match. + if (req.attributes && req.attributes.length > 0 && !req.attributes.includes('*')) { + res.attributes = req.attributes.map(a => a.toLowerCase()); + } + try { this.emit('searchRequest', { filter: filterStr, @@ -346,7 +353,15 @@ class LdapEngine extends EventEmitter { const users = await this.directoryProvider.getAllUsers(); this.logger.debug(`Found ${users.length} users`); + // Check if filtering by ldapPublicKey objectClass + const filterBySSHKeys = /objectClass=ldapPublicKey/i.test(filterStr); + for (const user of users) { + // Skip users without SSH keys if filtering by ldapPublicKey + if (filterBySSHKeys && !user.sshpublickey) { + continue; + } + const entry = createLdapEntry(user, this.config.baseDn); this.emit('entryFound', { type: 'user', entry: entry.dn }); res.send(entry); diff --git a/npm/src/utils/filterUtils.js b/npm/src/utils/filterUtils.js index 4aca601..295f358 100644 --- a/npm/src/utils/filterUtils.js +++ b/npm/src/utils/filterUtils.js @@ -57,7 +57,7 @@ function isAllUsersRequest(filterStr, attributes) { } // User objectClass searches - if (/(objectClass=posixAccount)|(objectClass=inetOrgPerson)|(objectClass=person)/i.test(filterStr)) { + if (/(objectClass=posixAccount)|(objectClass=inetOrgPerson)|(objectClass=person)|(objectClass=ldapPublicKey)/i.test(filterStr)) { return true; } diff --git a/npm/src/utils/ldapUtils.js b/npm/src/utils/ldapUtils.js index 8fbd8ce..42157c5 100644 --- a/npm/src/utils/ldapUtils.js +++ b/npm/src/utils/ldapUtils.js @@ -14,6 +14,7 @@ * @property {string} [mail] - User's email address * @property {string} [home_directory] - User's home directory * @property {string} [login_shell] - User's login shell (e.g. "/bin/bash") + * @property {string|string[]} [sshpublickey] - SSH public key(s) for openssh-lpk support */ /** @@ -81,6 +82,12 @@ function createLdapEntry(user, baseDn) { if (fullName) entry.attributes.gecos = fullName; + // SSH public key support (openssh-lpk schema) + if (user.sshpublickey) { + entry.attributes.sshPublicKey = user.sshpublickey; + entry.attributes.objectClass.push("ldapPublicKey"); + } + return entry; } diff --git a/npm/test/unit/utils/filterUtils.test.js b/npm/test/unit/utils/filterUtils.test.js index a5570de..b89bb3f 100644 --- a/npm/test/unit/utils/filterUtils.test.js +++ b/npm/test/unit/utils/filterUtils.test.js @@ -80,6 +80,11 @@ describe('filterUtils', () => { expect(result).toBe(true); }); + test('should detect ldapPublicKey objectClass filter: (objectClass=ldapPublicKey)', () => { + const result = isAllUsersRequest('(objectClass=ldapPublicKey)', []); + expect(result).toBe(true); + }); + test('should detect wildcard uid filter: (uid=*)', () => { const result = isAllUsersRequest('(uid=*)', []); expect(result).toBe(true); diff --git a/npm/test/unit/utils/ldapUtils.test.js b/npm/test/unit/utils/ldapUtils.test.js index 63d7ca3..56713f6 100644 --- a/npm/test/unit/utils/ldapUtils.test.js +++ b/npm/test/unit/utils/ldapUtils.test.js @@ -160,6 +160,55 @@ describe('ldapUtils', () => { expect(entry.attributes.sn).toBe('Smith'); expect(entry.attributes.gecos).toBe('Smith'); }); + + test('should include sshPublicKey attribute when sshpublickey field is provided', () => { + const user = { + username: 'sshuser', + uid_number: 3000, + gid_number: 3000, + sshpublickey: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKey... user@host' + }; + + const entry = createLdapEntry(user, baseDN); + + expect(entry.attributes.sshPublicKey).toBe('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKey... user@host'); + expect(entry.attributes.objectClass).toContain('ldapPublicKey'); + expect(entry.attributes.objectClass).toEqual(['top', 'posixAccount', 'inetOrgPerson', 'ldapPublicKey']); + }); + + test('should handle multiple SSH public keys as array', () => { + const user = { + username: 'multikey', + uid_number: 3001, + gid_number: 3001, + sshpublickey: [ + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKey1... user@host1', + 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKey2... user@host2' + ] + }; + + const entry = createLdapEntry(user, baseDN); + + expect(entry.attributes.sshPublicKey).toEqual([ + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKey1... user@host1', + 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKey2... user@host2' + ]); + expect(entry.attributes.objectClass).toContain('ldapPublicKey'); + }); + + test('should not include sshPublicKey or ldapPublicKey when sshpublickey is not provided', () => { + const user = { + username: 'nossh', + uid_number: 3002, + gid_number: 3002 + }; + + const entry = createLdapEntry(user, baseDN); + + expect(entry.attributes.sshPublicKey).toBeUndefined(); + expect(entry.attributes.objectClass).not.toContain('ldapPublicKey'); + expect(entry.attributes.objectClass).toEqual(['top', 'posixAccount', 'inetOrgPerson']); + }); }); describe('createLdapGroupEntry', () => { diff --git a/server/test/data/common.users.json b/server/test/data/common.users.json index 879eaf9..d587962 100644 --- a/server/test/data/common.users.json +++ b/server/test/data/common.users.json @@ -50,5 +50,19 @@ "home_directory": "/home/disabled", "login_shell": "/bin/bash", "enabled": false + }, + { + "username": "sshuser", + "password": "sshpass123", + "uid_number": 1004, + "gid_number": 1001, + "full_name": "SSH Test User", + "surname": "User", + "given_name": "SSH", + "mail": "sshuser@example.com", + "home_directory": "/home/sshuser", + "login_shell": "/bin/bash", + "enabled": true, + "sshpublickey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8... sshuser@testhost" } ] diff --git a/server/test/data/e2e.sssd.sql b/server/test/data/e2e.sssd.sql index 7fb8823..3ec9378 100644 --- a/server/test/data/e2e.sssd.sql +++ b/server/test/data/e2e.sssd.sql @@ -15,7 +15,8 @@ CREATE TABLE IF NOT EXISTS users ( uidNumber INT NOT NULL, gidNumber INT NOT NULL, homeDirectory VARCHAR(256) NOT NULL, - loginShell VARCHAR(64) NOT NULL DEFAULT '/bin/bash' + loginShell VARCHAR(64) NOT NULL DEFAULT '/bin/bash', + sshpublickey TEXT ); CREATE TABLE IF NOT EXISTS `groups` ( @@ -29,12 +30,13 @@ CREATE TABLE IF NOT EXISTS `groups` ( -- Password: 'password123' (bcrypt hashed with 10 rounds) -- Pre-hashed because this SQL is loaded directly by MySQL, not processed by Node.js -- Hash generated with: bcrypt.hash('password123', 10) -INSERT INTO users (uid, cn, sn, mail, userPassword, uidNumber, gidNumber, homeDirectory) +INSERT INTO users (uid, cn, sn, mail, userPassword, uidNumber, gidNumber, homeDirectory, sshpublickey) VALUES - ('testuser', 'Test User', 'User', 'testuser@example.com', '$2b$10$DJylnYTJZBhXqzYDV62nTOCW3/6ytjmXITpGo.tSqR5eCppmERflS', 10100, 20100, '/home/testuser'); + ('testuser', 'Test User', 'User', 'testuser@example.com', '$2b$10$DJylnYTJZBhXqzYDV62nTOCW3/6ytjmXITpGo.tSqR5eCppmERflS', 10100, 20100, '/home/testuser', 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKogUL8oT4Sn4+V2zBa4Jtis4CIryh+igq2PTCoYXSw4 testuser@e2e-test'), + ('nokeyuser', 'NoKey User', 'NoKey', 'nokeyuser@example.com', '$2b$10$DJylnYTJZBhXqzYDV62nTOCW3/6ytjmXITpGo.tSqR5eCppmERflS', 10101, 20100, '/home/nokeyuser', NULL); -- Test groups with testuser as member INSERT INTO `groups` (cn, gidNumber, member_uids) VALUES - ('developers', 20100, JSON_ARRAY('testuser')), + ('developers', 20100, JSON_ARRAY('testuser', 'nokeyuser')), ('devops', 20101, JSON_ARRAY('testuser')); diff --git a/server/test/e2e/README.md b/server/test/e2e/README.md index 1b61bf8..fa7f027 100644 --- a/server/test/e2e/README.md +++ b/server/test/e2e/README.md @@ -15,6 +15,11 @@ It validates full authentication flow: - `getent passwd testuser` returns correct home directory and shell - SSH authentication works end-to-end +It validates SSH public key retrieval via `sss_ssh_authorizedkeys`: +- `sss_ssh_authorizedkeys testuser` returns the correct SSH public key from LDAP +- `sss_ssh_authorizedkeys nokeyuser` returns empty for users without SSH keys +- SSH key-based authentication works using the key stored in LDAP + ## Test Classification - **Unit tests** (`test/unit/`): Fast, isolated tests with mocked dependencies diff --git a/server/test/e2e/client/Dockerfile b/server/test/e2e/client/Dockerfile index ee11e87..ff7536a 100644 --- a/server/test/e2e/client/Dockerfile +++ b/server/test/e2e/client/Dockerfile @@ -4,6 +4,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ openssh-server \ sssd \ sssd-ldap \ + sssd-tools \ ldap-utils \ sshpass \ procps \ @@ -16,9 +17,12 @@ RUN mkdir -p /var/run/sshd && \ echo "Port 2222" >> /etc/ssh/sshd_config && \ echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && \ echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config && \ + echo 'PubkeyAuthentication yes' >> /etc/ssh/sshd_config && \ echo 'UsePAM yes' >> /etc/ssh/sshd_config && \ echo 'LoginGraceTime 30' >> /etc/ssh/sshd_config && \ - echo 'MaxAuthTries 3' >> /etc/ssh/sshd_config + echo 'MaxAuthTries 3' >> /etc/ssh/sshd_config && \ + echo 'AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys %u' >> /etc/ssh/sshd_config && \ + echo 'AuthorizedKeysCommandUser nobody' >> /etc/ssh/sshd_config # PAM: create home dirs for LDAP users RUN echo "session required pam_mkhomedir.so skel=/etc/skel umask=0077" >> /etc/pam.d/sshd @@ -38,6 +42,10 @@ RUN useradd -m -s /bin/bash localadmin && echo 'localadmin:localpass' | chpasswd # Pre-create home directory for testuser to avoid warnings RUN mkdir -p /home/testuser && chmod 755 /home/testuser +# Copy test SSH private key for key-based auth testing +COPY fixtures/testuser_ed25519 /tmp/testuser_ed25519 +RUN chmod 600 /tmp/testuser_ed25519 + EXPOSE 2222 CMD service ssh start && /usr/sbin/sssd -i -d 9 diff --git a/server/test/e2e/client/fixtures/testuser_ed25519 b/server/test/e2e/client/fixtures/testuser_ed25519 new file mode 100644 index 0000000..cd57fea --- /dev/null +++ b/server/test/e2e/client/fixtures/testuser_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACCqIFC/KE+Ep+PldswWuCbYrOAiK8ofooKtj0wqGF0sOAAAAJgtlX/ILZV/ +yAAAAAtzc2gtZWQyNTUxOQAAACCqIFC/KE+Ep+PldswWuCbYrOAiK8ofooKtj0wqGF0sOA +AAAECyoNqVL1PFfHNetG/g7gV8CDpsiyofSVF9PxauoO78j6ogUL8oT4Sn4+V2zBa4Jtis +4CIryh+igq2PTCoYXSw4AAAAEXRlc3R1c2VyQGUyZS10ZXN0AQIDBA== +-----END OPENSSH PRIVATE KEY----- diff --git a/server/test/e2e/client/fixtures/testuser_ed25519.pub b/server/test/e2e/client/fixtures/testuser_ed25519.pub new file mode 100644 index 0000000..18c75c8 --- /dev/null +++ b/server/test/e2e/client/fixtures/testuser_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKogUL8oT4Sn4+V2zBa4Jtis4CIryh+igq2PTCoYXSw4 testuser@e2e-test diff --git a/server/test/e2e/client/sssd.conf b/server/test/e2e/client/sssd.conf index 328db1f..a25649f 100644 --- a/server/test/e2e/client/sssd.conf +++ b/server/test/e2e/client/sssd.conf @@ -1,5 +1,5 @@ [sssd] -services = nss, pam +services = nss, pam, ssh config_file_version = 2 domains = LDAP @@ -31,3 +31,6 @@ ldap_group_member = memberUid # NSS/PAM tuning entry_cache_timeout = 600 ldap_network_timeout = 3 + +# SSH public key retrieval +ldap_user_ssh_public_key = sshPublicKey diff --git a/server/test/e2e/docker-compose.yml b/server/test/e2e/docker-compose.yml index a6858b1..e6227b8 100644 --- a/server/test/e2e/docker-compose.yml +++ b/server/test/e2e/docker-compose.yml @@ -12,8 +12,8 @@ services: - TLS_ENABLE=true - TLS_CERT_PATH=/cert/server.crt - TLS_KEY_PATH=/cert/server.key - - "SQL_QUERY_ALL_USERS=SELECT uid AS username, cn AS full_name, sn AS surname, mail, homeDirectory AS home_directory, loginShell AS login_shell, uidNumber AS uid_number, gidNumber AS gid_number FROM users" - - "SQL_QUERY_ONE_USER=SELECT uid AS username, cn AS full_name, sn AS surname, mail, homeDirectory AS home_directory, loginShell AS login_shell, uidNumber AS uid_number, gidNumber AS gid_number, userPassword AS password FROM users WHERE uid = ?" + - "SQL_QUERY_ALL_USERS=SELECT uid AS username, cn AS full_name, sn AS surname, mail, homeDirectory AS home_directory, loginShell AS login_shell, uidNumber AS uid_number, gidNumber AS gid_number, sshpublickey FROM users" + - "SQL_QUERY_ONE_USER=SELECT uid AS username, cn AS full_name, sn AS surname, mail, homeDirectory AS home_directory, loginShell AS login_shell, uidNumber AS uid_number, gidNumber AS gid_number, sshpublickey, userPassword AS password FROM users WHERE uid = ?" - "SQL_QUERY_ALL_GROUPS=SELECT cn AS name, gidNumber AS gid_number, member_uids FROM `groups`" - "SQL_QUERY_GROUPS_BY_MEMBER=SELECT cn AS name, gidNumber AS gid_number, member_uids FROM `groups` WHERE JSON_CONTAINS(member_uids, JSON_QUOTE(?), '$')" ports: diff --git a/server/test/e2e/run-sssd-integration.sh b/server/test/e2e/run-sssd-integration.sh index d014c20..ba53861 100755 --- a/server/test/e2e/run-sssd-integration.sh +++ b/server/test/e2e/run-sssd-integration.sh @@ -96,7 +96,35 @@ else exit 1 fi +# Test 6: sss_ssh_authorizedkeys returns SSH public key for testuser +echo "Test 6: sss_ssh_authorizedkeys returns SSH key for testuser..." +SSH_KEY_OUTPUT=$(container_exec "sss_ssh_authorizedkeys testuser" || echo "FAIL") +echo " Result: $SSH_KEY_OUTPUT" +echo "$SSH_KEY_OUTPUT" | grep -q "ssh-ed25519" || { echo "FAIL: sss_ssh_authorizedkeys did not return SSH key"; exit 1; } +echo "$SSH_KEY_OUTPUT" | grep -q "AAAAC3NzaC1lZDI1NTE5AAAAIKogUL8oT4Sn4+V2zBa4Jtis4CIryh+igq2PTCoYXSw4" || { echo "FAIL: SSH key content mismatch"; exit 1; } +echo " ✓ SSH public key correctly retrieved via sss_ssh_authorizedkeys" + +# Test 7: sss_ssh_authorizedkeys returns empty for user without SSH key +echo "Test 7: sss_ssh_authorizedkeys returns empty for user without SSH key..." +NO_KEY_OUTPUT=$(container_exec "sss_ssh_authorizedkeys nokeyuser" || echo "") +echo " Result: '${NO_KEY_OUTPUT}'" +if [ -z "$NO_KEY_OUTPUT" ] || ! echo "$NO_KEY_OUTPUT" | grep -q "ssh-"; then + echo " ✓ No SSH key returned for user without key" +else + echo "FAIL: Unexpected SSH key returned for nokeyuser: $NO_KEY_OUTPUT" + exit 1 +fi + +# Test 8: SSH key-based authentication +echo "Test 8: SSH public key authentication..." +KEY_AUTH_OUTPUT=$(docker compose exec -T sssd-client bash -c "ssh -i /tmp/testuser_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no -p 2222 testuser@localhost 'whoami'" 2>/dev/null || echo "FAIL") +echo " Result: $KEY_AUTH_OUTPUT" +[[ "$KEY_AUTH_OUTPUT" == "testuser" ]] || { echo "FAIL: SSH key-based auth failed or whoami=$KEY_AUTH_OUTPUT"; exit 1; } +echo " ✓ SSH public key authentication successful" + echo "" echo "==========================================" echo "All SSH integration tests passed! ✓" +echo " Tests 1-5: Password auth, UID/GID, groups" +echo " Tests 6-8: SSH key via sss_ssh_authorizedkeys" echo "==========================================" diff --git a/server/test/e2e/sql/init.sql b/server/test/e2e/sql/init.sql index 8c78fe5..cbe570f 100644 --- a/server/test/e2e/sql/init.sql +++ b/server/test/e2e/sql/init.sql @@ -14,7 +14,8 @@ CREATE TABLE IF NOT EXISTS users ( uidNumber INT NOT NULL, gidNumber INT NOT NULL, homeDirectory VARCHAR(256) NOT NULL, - loginShell VARCHAR(64) NOT NULL DEFAULT '/bin/bash' + loginShell VARCHAR(64) NOT NULL DEFAULT '/bin/bash', + sshpublickey TEXT ); CREATE TABLE IF NOT EXISTS `groups` ( @@ -24,11 +25,12 @@ CREATE TABLE IF NOT EXISTS `groups` ( member_uids JSON NOT NULL ); -INSERT INTO users (uid, cn, sn, mail, userPassword, uidNumber, gidNumber, homeDirectory) +INSERT INTO users (uid, cn, sn, mail, userPassword, uidNumber, gidNumber, homeDirectory, sshpublickey) VALUES - ('testuser', 'Test User', 'User', 'testuser@example.com', '$2b$10$HV4N7iwiJsiyERmTieP69.wm./j0esYrr3XdJ1Q2QFqFC0qmhy65q', 10100, 20100, '/home/testuser'); + ('testuser', 'Test User', 'User', 'testuser@example.com', '$2b$10$HV4N7iwiJsiyERmTieP69.wm./j0esYrr3XdJ1Q2QFqFC0qmhy65q', 10100, 20100, '/home/testuser', 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKogUL8oT4Sn4+V2zBa4Jtis4CIryh+igq2PTCoYXSw4 testuser@e2e-test'), + ('nokeyuser', 'NoKey User', 'NoKey', 'nokeyuser@example.com', '$2b$10$HV4N7iwiJsiyERmTieP69.wm./j0esYrr3XdJ1Q2QFqFC0qmhy65q', 10101, 20100, '/home/nokeyuser', NULL); INSERT INTO `groups` (cn, gidNumber, member_uids) VALUES - ('developers', 20100, JSON_ARRAY('testuser')), + ('developers', 20100, JSON_ARRAY('testuser', 'nokeyuser')), ('devops', 20101, JSON_ARRAY('testuser')); diff --git a/server/test/integration/directory/mongodb.directory.test.js b/server/test/integration/directory/mongodb.directory.test.js index d3e47dd..d2daf84 100644 --- a/server/test/integration/directory/mongodb.directory.test.js +++ b/server/test/integration/directory/mongodb.directory.test.js @@ -131,14 +131,14 @@ maybeDescribe('MongoDB Directory Backend - Acceptance Tests', () => { scope: 'sub' }); - // Should return users (4) + groups (4) - expect(results.length).toBeGreaterThanOrEqual(8); + // Should return users (5) + groups (4) + expect(results.length).toBeGreaterThanOrEqual(9); // Verify we have both users and groups const userEntries = results.filter(r => r.dn.includes('uid=')); const groupEntries = results.filter(r => r.dn.includes('cn=')); - expect(userEntries.length).toBe(4); + expect(userEntries.length).toBe(5); expect(groupEntries.length).toBe(4); }); @@ -148,7 +148,7 @@ maybeDescribe('MongoDB Directory Backend - Acceptance Tests', () => { scope: 'sub' }); - expect(results.length).toBe(4); + expect(results.length).toBe(5); // Verify all results are user entries results.forEach(entry => { @@ -166,6 +166,7 @@ maybeDescribe('MongoDB Directory Backend - Acceptance Tests', () => { expect(usernames).toContain('admin'); expect(usernames).toContain('jdoe'); expect(usernames).toContain('disabled'); + expect(usernames).toContain('sshuser'); }); test('c. (objectClass=posixGroup) should return all groups', async () => { diff --git a/server/test/integration/directory/mysql.directory.test.js b/server/test/integration/directory/mysql.directory.test.js index 4d7a325..15750a2 100644 --- a/server/test/integration/directory/mysql.directory.test.js +++ b/server/test/integration/directory/mysql.directory.test.js @@ -45,8 +45,8 @@ maybeDescribe('MySQL Directory Backend (real DB) - Integration', () => { function configureEnv() { process.env.SQL_URI = url; process.env.SQL_SSL = 'false'; // Disable TLS for local testing - process.env.SQL_QUERY_ALL_USERS = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number FROM users'; - process.env.SQL_QUERY_ONE_USER = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number, password_hash AS password FROM users WHERE username = ?'; + process.env.SQL_QUERY_ALL_USERS = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number, sshpublickey FROM users'; + process.env.SQL_QUERY_ONE_USER = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number, sshpublickey, password_hash AS password FROM users WHERE username = ?'; process.env.SQL_QUERY_ALL_GROUPS = 'SELECT cn AS name, gid_number AS gid_number, member_uids FROM `groups`'; process.env.SQL_QUERY_GROUPS_BY_MEMBER = 'SELECT cn AS name, gid_number AS gid_number, member_uids FROM `groups` WHERE JSON_CONTAINS(member_uids, JSON_QUOTE(?), "$")'; } @@ -75,19 +75,19 @@ maybeDescribe('MySQL Directory Backend (real DB) - Integration', () => { test('a. (objectClass=*) should return all objects (users + groups)', async () => { await startServer(false); const results = await doSearch(client, acceptanceFilters.allObjects); - // 4 users + 4 groups = 8 - expect(results.length).toBeGreaterThanOrEqual(8); + // 5 users + 4 groups = 9 + expect(results.length).toBeGreaterThanOrEqual(9); const userEntries = results.filter(r => /uid=/.test(r.dn)); const groupEntries = results.filter(r => /cn=/.test(r.dn)); - expect(userEntries.length).toBe(4); + expect(userEntries.length).toBe(5); expect(groupEntries.length).toBe(4); }); test('b. (objectClass=posixAccount) should return all users', async () => { await startServer(false); const results = await doSearch(client, acceptanceFilters.allUsers); - // From common.users.json → 4 users - expect(results.length).toBe(4); + // From common.users.json → 5 users + expect(results.length).toBe(5); // Verify all results are user entries with required attributes results.forEach(entry => { @@ -111,6 +111,7 @@ maybeDescribe('MySQL Directory Backend (real DB) - Integration', () => { expect(usernames).toContain('admin'); expect(usernames).toContain('jdoe'); expect(usernames).toContain('disabled'); + expect(usernames).toContain('sshuser'); }); test('c. (objectClass=posixGroup) should return all groups', async () => { @@ -156,6 +157,21 @@ maybeDescribe('MySQL Directory Backend (real DB) - Integration', () => { expect(user.attributes.objectClass).toContain('inetOrgPerson'); }); + test('d2. (uid=sshuser) should return user with SSH public key', async () => { + await startServer(false); + const results = await doSearch(client, acceptanceFilters.specificUser('sshuser')); + expect(results.length).toBe(1); + + const user = results[0]; + expect(user.dn).toBe(`uid=sshuser,${baseDn}`); + expect(user.attributes.uid).toBe('sshuser'); + expect(user.attributes.cn).toBe('SSH Test User'); + expect(user.attributes.sshPublicKey).toBe('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8... sshuser@testhost'); + expect(user.attributes.objectClass).toContain('posixAccount'); + expect(user.attributes.objectClass).toContain('inetOrgPerson'); + expect(user.attributes.objectClass).toContain('ldapPublicKey'); + }); + test('e. (cn=groupname) should return specific group', async () => { await startServer(false); const results = await doSearch(client, acceptanceFilters.specificGroup('admins')); diff --git a/server/test/integration/directory/postgres.directory.test.js b/server/test/integration/directory/postgres.directory.test.js index e7a914d..f48fe3d 100644 --- a/server/test/integration/directory/postgres.directory.test.js +++ b/server/test/integration/directory/postgres.directory.test.js @@ -44,8 +44,8 @@ maybeDescribe('PostgreSQL Directory Backend (real DB) - Integration', () => { function configureEnv() { process.env.SQL_URI = url; - process.env.SQL_QUERY_ALL_USERS = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number FROM users'; - process.env.SQL_QUERY_ONE_USER = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number, password_hash AS password FROM users WHERE username = ?'; + process.env.SQL_QUERY_ALL_USERS = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number, sshpublickey FROM users'; + process.env.SQL_QUERY_ONE_USER = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number, sshpublickey, password_hash AS password FROM users WHERE username = ?'; process.env.SQL_QUERY_ALL_GROUPS = 'SELECT cn AS name, gid_number, member_uids FROM groups'; process.env.SQL_QUERY_GROUPS_BY_MEMBER = 'SELECT cn AS name, gid_number, member_uids FROM groups WHERE member_uids @> to_jsonb(?::text)'; } @@ -78,19 +78,19 @@ maybeDescribe('PostgreSQL Directory Backend (real DB) - Integration', () => { test('a. (objectClass=*) should return all objects (users + groups)', async () => { await startServer(false); const results = await doSearch(client, acceptanceFilters.allObjects); - // 4 users + 4 groups = 8 - expect(results.length).toBeGreaterThanOrEqual(8); + // 5 users + 4 groups = 9 + expect(results.length).toBeGreaterThanOrEqual(9); const userEntries = results.filter(r => /uid=/.test(r.dn)); const groupEntries = results.filter(r => /cn=/.test(r.dn)); - expect(userEntries.length).toBe(4); + expect(userEntries.length).toBe(5); expect(groupEntries.length).toBe(4); }); test('b. (objectClass=posixAccount) should return all users', async () => { await startServer(false); const results = await doSearch(client, acceptanceFilters.allUsers); - // From common.users.json → 4 users - expect(results.length).toBe(4); + // From common.users.json → 5 users + expect(results.length).toBe(5); // Verify all results are user entries with required attributes results.forEach(entry => { @@ -114,6 +114,7 @@ maybeDescribe('PostgreSQL Directory Backend (real DB) - Integration', () => { expect(usernames).toContain('admin'); expect(usernames).toContain('jdoe'); expect(usernames).toContain('disabled'); + expect(usernames).toContain('sshuser'); }); test('c. (objectClass=posixGroup) should return all groups', async () => { diff --git a/server/test/integration/directory/sqlite.directory.test.js b/server/test/integration/directory/sqlite.directory.test.js index 9a3a936..8d0c7e6 100644 --- a/server/test/integration/directory/sqlite.directory.test.js +++ b/server/test/integration/directory/sqlite.directory.test.js @@ -61,8 +61,8 @@ describe('SQLite Directory Backend (real DB) - Integration', () => { function configureEnv() { process.env.SQL_URI = `sqlite:${dbPath}`; - process.env.SQL_QUERY_ALL_USERS = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number FROM users'; - process.env.SQL_QUERY_ONE_USER = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number, password_hash AS password FROM users WHERE username = ?'; + process.env.SQL_QUERY_ALL_USERS = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number, sshpublickey FROM users'; + process.env.SQL_QUERY_ONE_USER = 'SELECT username, full_name, surname, mail, home_directory, login_shell, uid_number, gid_number, sshpublickey, password_hash AS password FROM users WHERE username = ?'; process.env.SQL_QUERY_ALL_GROUPS = 'SELECT cn AS name, gid_number AS gid_number, member_uids FROM `groups`'; // SQLite JSON containment check - member_uids is stored as JSON array string process.env.SQL_QUERY_GROUPS_BY_MEMBER = "SELECT cn AS name, gid_number AS gid_number, member_uids FROM `groups` WHERE json_extract(member_uids, '$') LIKE '%' || ? || '%'"; @@ -97,19 +97,19 @@ describe('SQLite Directory Backend (real DB) - Integration', () => { test('a. (objectClass=*) should return all objects (users + groups)', async () => { await startServer(false); const results = await doSearch(client, acceptanceFilters.allObjects); - // 4 users + 4 groups = 8 - expect(results.length).toBeGreaterThanOrEqual(8); + // 5 users + 4 groups = 9 + expect(results.length).toBeGreaterThanOrEqual(9); const userEntries = results.filter(r => /uid=/.test(r.dn)); const groupEntries = results.filter(r => /cn=/.test(r.dn)); - expect(userEntries.length).toBe(4); + expect(userEntries.length).toBe(5); expect(groupEntries.length).toBe(4); }); test('b. (objectClass=posixAccount) should return all users', async () => { await startServer(false); const results = await doSearch(client, acceptanceFilters.allUsers); - // From common.users.json → 4 users - expect(results.length).toBe(4); + // From common.users.json → 5 users + expect(results.length).toBe(5); // Verify all results are user entries with required attributes results.forEach(entry => { @@ -133,6 +133,7 @@ describe('SQLite Directory Backend (real DB) - Integration', () => { expect(usernames).toContain('admin'); expect(usernames).toContain('jdoe'); expect(usernames).toContain('disabled'); + expect(usernames).toContain('sshuser'); }); test('c. (objectClass=posixGroup) should return all groups', async () => { diff --git a/server/test/integration/engine/sshPublicKey.test.js b/server/test/integration/engine/sshPublicKey.test.js new file mode 100644 index 0000000..6f4344b --- /dev/null +++ b/server/test/integration/engine/sshPublicKey.test.js @@ -0,0 +1,351 @@ +const { LdapEngine, AuthProvider, DirectoryProvider } = require('@ldap-gateway/core'); +const ldap = require('ldapjs'); +const logger = require('../../utils/mockLogger'); + +// Minimal mock auth provider +class MockAuthProvider extends AuthProvider { + initialize() {} + async authenticate(username, password) { + return username === 'sshuser' && password === 'password'; + } + async cleanup() {} +} + +// Mock directory provider with SSH key support +class MockDirectoryProvider extends DirectoryProvider { + initialize() {} + async findUser(username) { + if (username === 'sshuser') { + return { + username: 'sshuser', + first_name: 'SSH', + last_name: 'User', + uid_number: 10001, + gid_number: 100, + mail: 'ssh@example.com', + sshpublickey: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8... sshuser@testhost' + }; + } + if (username === 'multikey') { + return { + username: 'multikey', + first_name: 'Multi', + last_name: 'Key', + uid_number: 10002, + gid_number: 100, + mail: 'multi@example.com', + sshpublickey: [ + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8... key1@host', + 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKey... key2@host' + ] + }; + } + if (username === 'nokey') { + return { + username: 'nokey', + first_name: 'No', + last_name: 'Key', + uid_number: 10003, + gid_number: 100, + mail: 'nokey@example.com' + }; + } + return null; + } + async getAllUsers() { + return [ + { + username: 'sshuser', + first_name: 'SSH', + last_name: 'User', + uid_number: 10001, + gid_number: 100, + mail: 'ssh@example.com', + sshpublickey: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8... sshuser@testhost' + }, + { + username: 'multikey', + first_name: 'Multi', + last_name: 'Key', + uid_number: 10002, + gid_number: 100, + mail: 'multi@example.com', + sshpublickey: [ + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8... key1@host', + 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKey... key2@host' + ] + }, + { + username: 'nokey', + first_name: 'No', + last_name: 'Key', + uid_number: 10003, + gid_number: 100, + mail: 'nokey@example.com' + } + ]; + } + async getAllGroups() { return []; } + async findGroups() { return []; } + async cleanup() {} +} + +jest.setTimeout(10000); + +// Helper function to extract attributes from LDAP entry +function extractAttributes(entry) { + return entry.attributes.reduce((acc, attr) => { + const values = attr.values || attr.vals || []; + acc[attr.type] = values.length === 1 ? values[0] : values; + return acc; + }, {}); +} + +describe('LdapEngine - SSH Public Key Support (openssh-lpk)', () => { + const baseDn = 'dc=example,dc=com'; + const port = 12399; + let engine; + + afterEach(async () => { + if (engine) { await engine.stop(); engine = null; } + }); + + test('User with single SSH key returns sshPublicKey attribute and ldapPublicKey objectClass', async () => { + engine = new LdapEngine({ + baseDn, + port, + requireAuthForSearch: false, + authProviders: [new MockAuthProvider()], + directoryProvider: new MockDirectoryProvider(), + logger, + }); + await engine.start(); + + const client = ldap.createClient({ url: `ldap://127.0.0.1:${port}` }); + try { + const result = await new Promise((resolve, reject) => { + const entries = []; + client.search(baseDn, { filter: '(uid=sshuser)', scope: 'sub' }, (err, res) => { + if (err) return reject(err); + res.on('searchEntry', (entry) => { + entries.push({ + dn: entry.objectName.toString(), + attributes: extractAttributes(entry) + }); + }); + res.on('error', (e) => reject(e)); + res.on('end', () => resolve(entries)); + }); + }); + + expect(result.length).toBe(1); + const user = result[0]; + + // Check DN + expect(user.dn).toBe(`uid=sshuser,${baseDn}`); + + // Check SSH public key is present + expect(user.attributes.sshPublicKey).toBe('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8... sshuser@testhost'); + + // Check ldapPublicKey objectClass is present + expect(user.attributes.objectClass).toBeDefined(); + const objectClasses = Array.isArray(user.attributes.objectClass) + ? user.attributes.objectClass + : [user.attributes.objectClass]; + expect(objectClasses).toContain('ldapPublicKey'); + expect(objectClasses).toContain('posixAccount'); + expect(objectClasses).toContain('inetOrgPerson'); + } finally { + await new Promise((resolve) => client.unbind(() => resolve())); + client.destroy(); + } + }); + + test('User with multiple SSH keys returns array of sshPublicKey values', async () => { + engine = new LdapEngine({ + baseDn, + port, + requireAuthForSearch: false, + authProviders: [new MockAuthProvider()], + directoryProvider: new MockDirectoryProvider(), + logger, + }); + await engine.start(); + + const client = ldap.createClient({ url: `ldap://127.0.0.1:${port}` }); + try { + const result = await new Promise((resolve, reject) => { + const entries = []; + client.search(baseDn, { filter: '(uid=multikey)', scope: 'sub' }, (err, res) => { + if (err) return reject(err); + res.on('searchEntry', (entry) => { + entries.push({ + dn: entry.objectName.toString(), + attributes: extractAttributes(entry) + }); + }); + res.on('error', (e) => reject(e)); + res.on('end', () => resolve(entries)); + }); + }); + + expect(result.length).toBe(1); + const user = result[0]; + + // Check SSH public keys array is present + expect(user.attributes.sshPublicKey).toBeDefined(); + expect(Array.isArray(user.attributes.sshPublicKey)).toBe(true); + expect(user.attributes.sshPublicKey.length).toBe(2); + expect(user.attributes.sshPublicKey).toContain('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8... key1@host'); + expect(user.attributes.sshPublicKey).toContain('ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKey... key2@host'); + + // Check ldapPublicKey objectClass is present + const objectClasses = Array.isArray(user.attributes.objectClass) + ? user.attributes.objectClass + : [user.attributes.objectClass]; + expect(objectClasses).toContain('ldapPublicKey'); + } finally { + await new Promise((resolve) => client.unbind(() => resolve())); + client.destroy(); + } + }); + + test('User without SSH key does not have sshPublicKey attribute or ldapPublicKey objectClass', async () => { + engine = new LdapEngine({ + baseDn, + port, + requireAuthForSearch: false, + authProviders: [new MockAuthProvider()], + directoryProvider: new MockDirectoryProvider(), + logger, + }); + await engine.start(); + + const client = ldap.createClient({ url: `ldap://127.0.0.1:${port}` }); + try { + const result = await new Promise((resolve, reject) => { + const entries = []; + client.search(baseDn, { filter: '(uid=nokey)', scope: 'sub' }, (err, res) => { + if (err) return reject(err); + res.on('searchEntry', (entry) => { + entries.push({ + dn: entry.objectName.toString(), + attributes: extractAttributes(entry) + }); + }); + res.on('error', (e) => reject(e)); + res.on('end', () => resolve(entries)); + }); + }); + + expect(result.length).toBe(1); + const user = result[0]; + + // Check SSH public key is NOT present + expect(user.attributes.sshPublicKey).toBeUndefined(); + + // Check ldapPublicKey objectClass is NOT present + const objectClasses = Array.isArray(user.attributes.objectClass) + ? user.attributes.objectClass + : [user.attributes.objectClass]; + expect(objectClasses).not.toContain('ldapPublicKey'); + expect(objectClasses).toContain('posixAccount'); + expect(objectClasses).toContain('inetOrgPerson'); + } finally { + await new Promise((resolve) => client.unbind(() => resolve())); + client.destroy(); + } + }); + + test('Search for users with ldapPublicKey objectClass returns only users with SSH keys', async () => { + engine = new LdapEngine({ + baseDn, + port, + requireAuthForSearch: false, + authProviders: [new MockAuthProvider()], + directoryProvider: new MockDirectoryProvider(), + logger, + }); + await engine.start(); + + const client = ldap.createClient({ url: `ldap://127.0.0.1:${port}` }); + try { + const result = await new Promise((resolve, reject) => { + const entries = []; + client.search(baseDn, { filter: '(objectClass=ldapPublicKey)', scope: 'sub' }, (err, res) => { + if (err) return reject(err); + res.on('searchEntry', (entry) => { + entries.push({ + dn: entry.objectName.toString(), + attributes: extractAttributes(entry) + }); + }); + res.on('error', (e) => reject(e)); + res.on('end', () => resolve(entries)); + }); + }); + + // Should return 2 users: sshuser and multikey + expect(result.length).toBe(2); + + const usernames = result.map(e => e.attributes.uid); + expect(usernames).toContain('sshuser'); + expect(usernames).toContain('multikey'); + + // All returned users should have sshPublicKey attribute + result.forEach(user => { + expect(user.attributes.sshPublicKey).toBeDefined(); + }); + } finally { + await new Promise((resolve) => client.unbind(() => resolve())); + client.destroy(); + } + }); + + test('Requesting all attributes with * returns sshPublicKey', async () => { + engine = new LdapEngine({ + baseDn, + port, + requireAuthForSearch: false, + authProviders: [new MockAuthProvider()], + directoryProvider: new MockDirectoryProvider(), + logger, + }); + await engine.start(); + + const client = ldap.createClient({ url: `ldap://127.0.0.1:${port}` }); + try { + const result = await new Promise((resolve, reject) => { + const entries = []; + client.search(baseDn, { + filter: '(uid=sshuser)', + scope: 'sub', + attributes: ['*'] // Request all user attributes (SSSD pattern) + }, (err, res) => { + if (err) return reject(err); + res.on('searchEntry', (entry) => { + entries.push({ + dn: entry.objectName.toString(), + attributes: extractAttributes(entry) + }); + }); + res.on('error', (e) => reject(e)); + res.on('end', () => resolve(entries)); + }); + }); + + expect(result.length).toBe(1); + const user = result[0]; + + // Should have all user attributes including sshPublicKey + expect(user.attributes.uid).toBe('sshuser'); + expect(user.attributes.sshPublicKey).toBe('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8... sshuser@testhost'); + + // objectClass is always returned + expect(user.attributes.objectClass).toBeDefined(); + } finally { + await new Promise((resolve) => client.unbind(() => resolve())); + client.destroy(); + } + }); +}); diff --git a/server/test/utils/dbSeeder.js b/server/test/utils/dbSeeder.js index d0adb3a..8e6561e 100644 --- a/server/test/utils/dbSeeder.js +++ b/server/test/utils/dbSeeder.js @@ -27,7 +27,8 @@ class SQLiteSeeder { mail TEXT, home_directory TEXT, login_shell TEXT, - enabled INTEGER DEFAULT 1 + enabled INTEGER DEFAULT 1, + sshpublickey TEXT ) `); @@ -51,8 +52,8 @@ class SQLiteSeeder { const hash = await bcrypt.hash(user.password, 10); await this.db.run( `INSERT OR REPLACE INTO users - (username, password_hash, uid_number, gid_number, full_name, surname, given_name, mail, home_directory, login_shell, enabled) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + (username, password_hash, uid_number, gid_number, full_name, surname, given_name, mail, home_directory, login_shell, enabled, sshpublickey) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, user.username, hash, user.uid_number, @@ -63,7 +64,8 @@ class SQLiteSeeder { user.mail, user.home_directory, user.login_shell, - user.enabled ? 1 : 0 + user.enabled ? 1 : 0, + user.sshpublickey || null ); } @@ -109,6 +111,7 @@ class MySQLSeeder { home_directory VARCHAR(255), login_shell VARCHAR(255), enabled BOOLEAN DEFAULT TRUE, + sshpublickey TEXT, INDEX idx_username (username), INDEX idx_uid (uid_number) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 @@ -136,12 +139,13 @@ class MySQLSeeder { const hash = await bcrypt.hash(user.password, 10); await this.connection.execute(` INSERT INTO users - (username, password_hash, uid_number, gid_number, full_name, surname, given_name, mail, home_directory, login_shell, enabled) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (username, password_hash, uid_number, gid_number, full_name, surname, given_name, mail, home_directory, login_shell, enabled, sshpublickey) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE password_hash = VALUES(password_hash), uid_number = VALUES(uid_number), - gid_number = VALUES(gid_number) + gid_number = VALUES(gid_number), + sshpublickey = VALUES(sshpublickey) `, [ user.username, hash, @@ -153,7 +157,8 @@ class MySQLSeeder { user.mail, user.home_directory, user.login_shell, - user.enabled + user.enabled, + user.sshpublickey || null ]); } @@ -210,7 +215,8 @@ class MongoDBSeeder { mail: user.mail, home_directory: user.home_directory, login_shell: user.login_shell, - enabled: user.enabled + enabled: user.enabled, + sshpublickey: user.sshpublickey || null })) ); @@ -267,7 +273,8 @@ class PostgreSQLSeeder { mail TEXT, home_directory TEXT, login_shell TEXT, - enabled BOOLEAN DEFAULT TRUE + enabled BOOLEAN DEFAULT TRUE, + sshpublickey TEXT ) `); @@ -295,12 +302,13 @@ class PostgreSQLSeeder { const hash = await bcrypt.hash(user.password, 10); await this.client.query(` INSERT INTO users - (username, password_hash, uid_number, gid_number, full_name, surname, given_name, mail, home_directory, login_shell, enabled) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + (username, password_hash, uid_number, gid_number, full_name, surname, given_name, mail, home_directory, login_shell, enabled, sshpublickey) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (username) DO UPDATE SET password_hash = EXCLUDED.password_hash, uid_number = EXCLUDED.uid_number, - gid_number = EXCLUDED.gid_number + gid_number = EXCLUDED.gid_number, + sshpublickey = EXCLUDED.sshpublickey `, [ user.username, hash, @@ -312,7 +320,8 @@ class PostgreSQLSeeder { user.mail, user.home_directory, user.login_shell, - user.enabled + user.enabled, + user.sshpublickey || null ]); }