@anddev-oss/verdaccio-auth-az-tables
Version:
Auth plugin for Verdaccio that utilises Azure Storage Account Tables
204 lines • 9.48 kB
JavaScript
;
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