UNPKG

@anddev-oss/verdaccio-auth-az-tables

Version:

Auth plugin for Verdaccio that utilises Azure Storage Account Tables

204 lines 9.48 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const debug_1 = __importDefault(require("debug")); const commons_api_1 = require("@verdaccio/commons-api"); const data_tables_1 = require("@azure/data-tables"); const identity_1 = require("@azure/identity"); const argon2_1 = __importDefault(require("argon2")); const debug = (0, debug_1.default)('verdaccio:plugin:azureTablesAuth'); class AuthCustomPlugin { logger; usersTableClient; groupsTableClient; constructor(config, options) { this.logger = options.logger; // Initialize Azure Table Storage clients this.initializeTableClients(config.azure); } initializeTableClients(azureConfig) { if (!azureConfig.accountName || !azureConfig.usersTable || !azureConfig.groupsTable) { throw new Error('Azure configuration must include accountName, usersTable, and groupsTable.'); } const serviceUrl = `https://${azureConfig.accountName}.table.core.windows.net`; let credential; if (azureConfig.accountKey) { debug(`Account Key Authentication`); credential = new data_tables_1.AzureNamedKeyCredential(azureConfig.accountName, azureConfig.accountKey); } else if (azureConfig.sasToken) { debug(`SAS Token Authentication`); credential = new data_tables_1.AzureSASCredential(azureConfig.sasToken); } else { // Default to DefaultAzureCredential (e.g., system-assigned identity or fallback) debug(`Fallback Default Azure Credential Authentication`); credential = new identity_1.DefaultAzureCredential(); } this.usersTableClient = new data_tables_1.TableClient(serviceUrl, azureConfig.usersTable, credential); this.groupsTableClient = new data_tables_1.TableClient(serviceUrl, azureConfig.groupsTable, credential); } authenticate(user, password, cb) { this.usersTableClient.getEntity("User", user) .then((userEntity) => { if (!userEntity || !userEntity.Password) { debug(`User ${user} not found or missing password`); return cb(null, false); // Indicate failure } const hashedPassword = userEntity.Password; this.verifyPassword(password, hashedPassword) .then((isValid) => { if (isValid) { // Parse groups from the user entity const groups = JSON.parse(userEntity.Groups) || []; debug(`User ${user} authenticated successfully. Groups: ${groups}`); return cb(null, groups); // Indicate success with groups } else { debug(`Password mismatch for user ${user}`); return cb(null, false); // Indicate failure } }) .catch((passwordError) => { this.logger.error({ error: passwordError }, 'Error verifying password'); return cb((0, commons_api_1.getInternalError)('Error verifying password'), false); }); }) .catch((error) => { this.logger.error({ error }, 'Error retrieving user'); return cb((0, commons_api_1.getInternalError)('Authentication error'), false); // Indicate failure on error }); } allow_access(user, pkg, cb) { const isAllowed = pkg?.access?.some(group => user.groups.includes(group)); return cb(null, isAllowed || false); } allow_publish(user, pkg, cb) { const isAllowed = pkg?.publish?.some(group => user.groups.includes(group)); return cb(null, isAllowed || false); } allow_unpublish(user, pkg, cb) { const isAllowed = pkg?.publish?.some(group => user.groups.includes(group)); return cb(null, isAllowed || false); } async getUserGroups(username) { try { const groupEntities = this.groupsTableClient.listEntities({ queryOptions: { filter: `PartitionKey eq 'Group' and RowKey eq '${username}'` } }); const groups = []; for await (const entity of groupEntities) { if (entity.GroupName) { groups.push(entity.GroupName); } } return groups; } catch (error) { this.logger.error({ error }, 'Error retrieving user groups'); return []; } } async adduser(username, password, cb) { debug(`adduser ${username}`); try { // Default group for new users const defaultGroup = 'users'; // Check if the user already exists const existingUser = await this.usersTableClient.getEntity("User", username).catch(() => null); if (existingUser) { debug(`User ${username} already exists.`); return cb((0, commons_api_1.getInternalError)('User already exists'), false); // Handle duplicate user } // Hash the password const hashedPassword = await this.hashPassword(password); // Add user entity with an empty group array await this.usersTableClient.createEntity({ partitionKey: 'User', rowKey: username, Password: hashedPassword, Groups: JSON.stringify([defaultGroup]), // Initialize with default group CreatedAt: new Date().toISOString(), }); debug(`User ${username} added successfully.`); // Ensure the default group exists in the verdaccioGroups table const groupPartitionKey = 'Group'; const groupRowKey = defaultGroup; const existingGroup = await this.groupsTableClient.getEntity(groupPartitionKey, groupRowKey).catch(() => null); if (!existingGroup) { debug(`Group ${defaultGroup} does not exist. Creating it.`); await this.groupsTableClient.createEntity({ partitionKey: groupPartitionKey, rowKey: groupRowKey, GroupName: defaultGroup, Description: 'Default group for new users', CreatedAt: new Date().toISOString(), }); debug(`Group ${defaultGroup} created successfully.`); } else { debug(`Group ${defaultGroup} already exists.`); } return cb(null, []); // Success } catch (error) { this.logger.error({ error }, `Error adding user ${username}`); return cb((0, commons_api_1.getInternalError)('Failed to add user'), false); // Failure } } async addUserToGroups(username, groups) { try { // Fetch the existing user entity const userEntity = await this.usersTableClient.getEntity("User", username); if (!userEntity) { throw new Error(`User ${username} does not exist.`); } // Parse existing groups and merge with new ones const existingGroups = JSON.parse(userEntity.Groups) || []; const updatedGroups = Array.from(new Set([...existingGroups, ...groups])); // Deduplicate // Update user entity with new groups await this.usersTableClient.updateEntity({ partitionKey: 'User', rowKey: username, Groups: JSON.stringify(updatedGroups), // Update groups as JSON array }, 'Merge'); debug(`User ${username} updated with groups: ${updatedGroups}`); // Ensure all groups exist in the verdaccioGroups table for (const group of groups) { const groupPartitionKey = 'Group'; const groupRowKey = group; const existingGroup = await this.groupsTableClient.getEntity(groupPartitionKey, groupRowKey).catch(() => null); if (!existingGroup) { debug(`Group ${group} does not exist. Creating it.`); await this.groupsTableClient.createEntity({ partitionKey: groupPartitionKey, rowKey: groupRowKey, GroupName: group, Description: `Group ${group}`, CreatedAt: new Date().toISOString(), }); debug(`Group ${group} created successfully.`); } } } catch (error) { this.logger.error({ error }, `Error updating user ${username} with groups`); throw error; } } async hashPassword(password) { return await argon2_1.default.hash(password); } async verifyPassword(password, hashedPassword) { try { // argon2.verify returns true if the password matches the hash return await argon2_1.default.verify(hashedPassword, password); } catch (error) { this.logger.error({ error }, 'Error verifying password'); return false; // Return false on error } } } exports.default = AuthCustomPlugin; //# sourceMappingURL=index.js.map