mirror of
https://github.com/wekan/wekan.git
synced 2026-02-15 04:28:07 +01:00
Merge branch 'main' into wekan-accounts-sandstorm-async-migration
This commit is contained in:
commit
89ce2f825d
316 changed files with 12991 additions and 3209 deletions
|
|
@ -93,6 +93,8 @@ Meteor.loginWithCas = function(options, callback) {
|
|||
};
|
||||
|
||||
var openCenteredPopup = function(url, width, height) {
|
||||
// #FIXME screenX and outerWidth are often different units on mobile screen or high DPI
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
|
||||
var screenX = typeof window.screenX !== 'undefined'
|
||||
? window.screenX : window.screenLeft;
|
||||
var screenY = typeof window.screenY !== 'undefined'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Package.describe({
|
||||
name: 'wekan-accounts-lockout',
|
||||
version: '1.0.0',
|
||||
version: '1.1.0',
|
||||
summary: 'Meteor package for locking user accounts and stopping brute force attacks',
|
||||
git: 'https://github.com/lucasantoniassi/meteor-accounts-lockout.git',
|
||||
documentation: 'README.md',
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ class KnownUser {
|
|||
this.settings = settings;
|
||||
}
|
||||
|
||||
startup() {
|
||||
async startup() {
|
||||
if (!(this.unchangedSettings instanceof Function)) {
|
||||
this.updateSettings();
|
||||
}
|
||||
this.scheduleUnlocksForLockedAccounts();
|
||||
KnownUser.unlockAccountsIfLockoutAlreadyExpired();
|
||||
await this.scheduleUnlocksForLockedAccounts();
|
||||
await KnownUser.unlockAccountsIfLockoutAlreadyExpired();
|
||||
this.hookIntoAccounts();
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ class KnownUser {
|
|||
}
|
||||
}
|
||||
|
||||
scheduleUnlocksForLockedAccounts() {
|
||||
async scheduleUnlocksForLockedAccounts() {
|
||||
const lockedAccountsCursor = Meteor.users.find(
|
||||
{
|
||||
'services.accounts-lockout.unlockTime': {
|
||||
|
|
@ -63,7 +63,7 @@ class KnownUser {
|
|||
},
|
||||
);
|
||||
const currentTime = Number(new Date());
|
||||
lockedAccountsCursor.forEach((user) => {
|
||||
for await (const user of lockedAccountsCursor) {
|
||||
let lockDuration = KnownUser.unlockTime(user) - currentTime;
|
||||
if (lockDuration >= this.settings.lockoutPeriod) {
|
||||
lockDuration = this.settings.lockoutPeriod * 1000;
|
||||
|
|
@ -75,10 +75,10 @@ class KnownUser {
|
|||
KnownUser.unlockAccount.bind(null, user._id),
|
||||
lockDuration,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static unlockAccountsIfLockoutAlreadyExpired() {
|
||||
static async unlockAccountsIfLockoutAlreadyExpired() {
|
||||
const currentTime = Number(new Date());
|
||||
const query = {
|
||||
'services.accounts-lockout.unlockTime': {
|
||||
|
|
@ -91,7 +91,7 @@ class KnownUser {
|
|||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
await Meteor.users.updateAsync(query, data);
|
||||
}
|
||||
|
||||
hookIntoAccounts() {
|
||||
|
|
@ -100,7 +100,7 @@ class KnownUser {
|
|||
}
|
||||
|
||||
|
||||
validateLoginAttempt(loginInfo) {
|
||||
async validateLoginAttempt(loginInfo) {
|
||||
if (
|
||||
// don't interrupt non-password logins
|
||||
loginInfo.type !== 'password' ||
|
||||
|
|
@ -130,12 +130,12 @@ class KnownUser {
|
|||
const canReset = (currentTime - firstFailedAttempt) > (1000 * this.settings.failureWindow);
|
||||
if (canReset) {
|
||||
failedAttempts = 1;
|
||||
KnownUser.resetAttempts(failedAttempts, userId);
|
||||
await KnownUser.resetAttempts(failedAttempts, userId);
|
||||
}
|
||||
|
||||
const canIncrement = failedAttempts < this.settings.failuresBeforeLockout;
|
||||
if (canIncrement) {
|
||||
KnownUser.incrementAttempts(failedAttempts, userId);
|
||||
await KnownUser.incrementAttempts(failedAttempts, userId);
|
||||
}
|
||||
|
||||
const maxAttemptsAllowed = this.settings.failuresBeforeLockout;
|
||||
|
|
@ -147,7 +147,7 @@ class KnownUser {
|
|||
KnownUser.tooManyAttempts(duration);
|
||||
}
|
||||
if (failedAttempts === maxAttemptsAllowed) {
|
||||
this.setNewUnlockTime(failedAttempts, userId);
|
||||
await this.setNewUnlockTime(failedAttempts, userId);
|
||||
|
||||
let duration = this.settings.lockoutPeriod;
|
||||
duration = Math.ceil(duration);
|
||||
|
|
@ -161,7 +161,7 @@ class KnownUser {
|
|||
);
|
||||
}
|
||||
|
||||
static resetAttempts(
|
||||
static async resetAttempts(
|
||||
failedAttempts,
|
||||
userId,
|
||||
) {
|
||||
|
|
@ -174,10 +174,10 @@ class KnownUser {
|
|||
'services.accounts-lockout.firstFailedAttempt': currentTime,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
await Meteor.users.updateAsync(query, data);
|
||||
}
|
||||
|
||||
static incrementAttempts(
|
||||
static async incrementAttempts(
|
||||
failedAttempts,
|
||||
userId,
|
||||
) {
|
||||
|
|
@ -189,10 +189,10 @@ class KnownUser {
|
|||
'services.accounts-lockout.lastFailedAttempt': currentTime,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
await Meteor.users.updateAsync(query, data);
|
||||
}
|
||||
|
||||
setNewUnlockTime(
|
||||
async setNewUnlockTime(
|
||||
failedAttempts,
|
||||
userId,
|
||||
) {
|
||||
|
|
@ -206,14 +206,14 @@ class KnownUser {
|
|||
'services.accounts-lockout.unlockTime': newUnlockTime,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
await Meteor.users.updateAsync(query, data);
|
||||
Meteor.setTimeout(
|
||||
KnownUser.unlockAccount.bind(null, userId),
|
||||
this.settings.lockoutPeriod * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
static onLogin(loginInfo) {
|
||||
static async onLogin(loginInfo) {
|
||||
if (loginInfo.type !== 'password') {
|
||||
return;
|
||||
}
|
||||
|
|
@ -225,7 +225,7 @@ class KnownUser {
|
|||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
await Meteor.users.updateAsync(query, data);
|
||||
}
|
||||
|
||||
static incorrectPassword(
|
||||
|
|
@ -306,7 +306,7 @@ class KnownUser {
|
|||
return firstFailedAttempt || 0;
|
||||
}
|
||||
|
||||
static unlockAccount(userId) {
|
||||
static async unlockAccount(userId) {
|
||||
const query = { _id: userId };
|
||||
const data = {
|
||||
$unset: {
|
||||
|
|
@ -314,7 +314,7 @@ class KnownUser {
|
|||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
await Meteor.users.updateAsync(query, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ class UnknownUser {
|
|||
this.settings = settings;
|
||||
}
|
||||
|
||||
startup() {
|
||||
async startup() {
|
||||
if (!(this.settings instanceof Function)) {
|
||||
this.updateSettings();
|
||||
}
|
||||
this.scheduleUnlocksForLockedAccounts();
|
||||
this.unlockAccountsIfLockoutAlreadyExpired();
|
||||
await this.scheduleUnlocksForLockedAccounts();
|
||||
await this.unlockAccountsIfLockoutAlreadyExpired();
|
||||
this.hookIntoAccounts();
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ class UnknownUser {
|
|||
}
|
||||
}
|
||||
|
||||
scheduleUnlocksForLockedAccounts() {
|
||||
async scheduleUnlocksForLockedAccounts() {
|
||||
const lockedAccountsCursor = this.AccountsLockoutCollection.find(
|
||||
{
|
||||
'services.accounts-lockout.unlockTime': {
|
||||
|
|
@ -67,8 +67,8 @@ class UnknownUser {
|
|||
},
|
||||
);
|
||||
const currentTime = Number(new Date());
|
||||
lockedAccountsCursor.forEach((connection) => {
|
||||
let lockDuration = this.unlockTime(connection) - currentTime;
|
||||
for await (const connection of lockedAccountsCursor) {
|
||||
let lockDuration = await this.unlockTime(connection) - currentTime;
|
||||
if (lockDuration >= this.settings.lockoutPeriod) {
|
||||
lockDuration = this.settings.lockoutPeriod * 1000;
|
||||
}
|
||||
|
|
@ -79,10 +79,10 @@ class UnknownUser {
|
|||
this.unlockAccount.bind(this, connection.clientAddress),
|
||||
lockDuration,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unlockAccountsIfLockoutAlreadyExpired() {
|
||||
async unlockAccountsIfLockoutAlreadyExpired() {
|
||||
const currentTime = Number(new Date());
|
||||
const query = {
|
||||
'services.accounts-lockout.unlockTime': {
|
||||
|
|
@ -95,7 +95,7 @@ class UnknownUser {
|
|||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.update(query, data);
|
||||
await this.AccountsLockoutCollection.updateAsync(query, data);
|
||||
}
|
||||
|
||||
hookIntoAccounts() {
|
||||
|
|
@ -103,7 +103,7 @@ class UnknownUser {
|
|||
Accounts.onLogin(this.onLogin.bind(this));
|
||||
}
|
||||
|
||||
validateLoginAttempt(loginInfo) {
|
||||
async validateLoginAttempt(loginInfo) {
|
||||
// don't interrupt non-password logins
|
||||
if (
|
||||
loginInfo.type !== 'password' ||
|
||||
|
|
@ -120,20 +120,20 @@ class UnknownUser {
|
|||
}
|
||||
|
||||
const clientAddress = loginInfo.connection.clientAddress;
|
||||
const unlockTime = this.unlockTime(loginInfo.connection);
|
||||
let failedAttempts = 1 + this.failedAttempts(loginInfo.connection);
|
||||
const firstFailedAttempt = this.firstFailedAttempt(loginInfo.connection);
|
||||
const unlockTime = await this.unlockTime(loginInfo.connection);
|
||||
let failedAttempts = 1 + await this.failedAttempts(loginInfo.connection);
|
||||
const firstFailedAttempt = await this.firstFailedAttempt(loginInfo.connection);
|
||||
const currentTime = Number(new Date());
|
||||
|
||||
const canReset = (currentTime - firstFailedAttempt) > (1000 * this.settings.failureWindow);
|
||||
if (canReset) {
|
||||
failedAttempts = 1;
|
||||
this.resetAttempts(failedAttempts, clientAddress);
|
||||
await this.resetAttempts(failedAttempts, clientAddress);
|
||||
}
|
||||
|
||||
const canIncrement = failedAttempts < this.settings.failuresBeforeLockout;
|
||||
if (canIncrement) {
|
||||
this.incrementAttempts(failedAttempts, clientAddress);
|
||||
await this.incrementAttempts(failedAttempts, clientAddress);
|
||||
}
|
||||
|
||||
const maxAttemptsAllowed = this.settings.failuresBeforeLockout;
|
||||
|
|
@ -145,7 +145,7 @@ class UnknownUser {
|
|||
UnknownUser.tooManyAttempts(duration);
|
||||
}
|
||||
if (failedAttempts === maxAttemptsAllowed) {
|
||||
this.setNewUnlockTime(failedAttempts, clientAddress);
|
||||
await this.setNewUnlockTime(failedAttempts, clientAddress);
|
||||
|
||||
let duration = this.settings.lockoutPeriod;
|
||||
duration = Math.ceil(duration);
|
||||
|
|
@ -159,7 +159,7 @@ class UnknownUser {
|
|||
);
|
||||
}
|
||||
|
||||
resetAttempts(
|
||||
async resetAttempts(
|
||||
failedAttempts,
|
||||
clientAddress,
|
||||
) {
|
||||
|
|
@ -172,10 +172,10 @@ class UnknownUser {
|
|||
'services.accounts-lockout.firstFailedAttempt': currentTime,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.upsert(query, data);
|
||||
await this.AccountsLockoutCollection.upsertAsync(query, data);
|
||||
}
|
||||
|
||||
incrementAttempts(
|
||||
async incrementAttempts(
|
||||
failedAttempts,
|
||||
clientAddress,
|
||||
) {
|
||||
|
|
@ -187,10 +187,10 @@ class UnknownUser {
|
|||
'services.accounts-lockout.lastFailedAttempt': currentTime,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.upsert(query, data);
|
||||
await this.AccountsLockoutCollection.upsertAsync(query, data);
|
||||
}
|
||||
|
||||
setNewUnlockTime(
|
||||
async setNewUnlockTime(
|
||||
failedAttempts,
|
||||
clientAddress,
|
||||
) {
|
||||
|
|
@ -204,14 +204,14 @@ class UnknownUser {
|
|||
'services.accounts-lockout.unlockTime': newUnlockTime,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.upsert(query, data);
|
||||
await this.AccountsLockoutCollection.upsertAsync(query, data);
|
||||
Meteor.setTimeout(
|
||||
this.unlockAccount.bind(this, clientAddress),
|
||||
this.settings.lockoutPeriod * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
onLogin(loginInfo) {
|
||||
async onLogin(loginInfo) {
|
||||
if (loginInfo.type !== 'password') {
|
||||
return;
|
||||
}
|
||||
|
|
@ -223,7 +223,7 @@ class UnknownUser {
|
|||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.update(query, data);
|
||||
await this.AccountsLockoutCollection.updateAsync(query, data);
|
||||
}
|
||||
|
||||
static userNotFound(
|
||||
|
|
@ -264,14 +264,14 @@ class UnknownUser {
|
|||
return unknownUsers || false;
|
||||
}
|
||||
|
||||
findOneByConnection(connection) {
|
||||
return this.AccountsLockoutCollection.findOne({
|
||||
async findOneByConnection(connection) {
|
||||
return await this.AccountsLockoutCollection.findOneAsync({
|
||||
clientAddress: connection.clientAddress,
|
||||
});
|
||||
}
|
||||
|
||||
unlockTime(connection) {
|
||||
connection = this.findOneByConnection(connection);
|
||||
async unlockTime(connection) {
|
||||
connection = await this.findOneByConnection(connection);
|
||||
let unlockTime;
|
||||
try {
|
||||
unlockTime = connection.services['accounts-lockout'].unlockTime;
|
||||
|
|
@ -281,8 +281,8 @@ class UnknownUser {
|
|||
return unlockTime || 0;
|
||||
}
|
||||
|
||||
failedAttempts(connection) {
|
||||
connection = this.findOneByConnection(connection);
|
||||
async failedAttempts(connection) {
|
||||
connection = await this.findOneByConnection(connection);
|
||||
let failedAttempts;
|
||||
try {
|
||||
failedAttempts = connection.services['accounts-lockout'].failedAttempts;
|
||||
|
|
@ -292,8 +292,8 @@ class UnknownUser {
|
|||
return failedAttempts || 0;
|
||||
}
|
||||
|
||||
lastFailedAttempt(connection) {
|
||||
connection = this.findOneByConnection(connection);
|
||||
async lastFailedAttempt(connection) {
|
||||
connection = await this.findOneByConnection(connection);
|
||||
let lastFailedAttempt;
|
||||
try {
|
||||
lastFailedAttempt = connection.services['accounts-lockout'].lastFailedAttempt;
|
||||
|
|
@ -303,8 +303,8 @@ class UnknownUser {
|
|||
return lastFailedAttempt || 0;
|
||||
}
|
||||
|
||||
firstFailedAttempt(connection) {
|
||||
connection = this.findOneByConnection(connection);
|
||||
async firstFailedAttempt(connection) {
|
||||
connection = await this.findOneByConnection(connection);
|
||||
let firstFailedAttempt;
|
||||
try {
|
||||
firstFailedAttempt = connection.services['accounts-lockout'].firstFailedAttempt;
|
||||
|
|
@ -314,7 +314,7 @@ class UnknownUser {
|
|||
return firstFailedAttempt || 0;
|
||||
}
|
||||
|
||||
unlockAccount(clientAddress) {
|
||||
async unlockAccount(clientAddress) {
|
||||
const query = { clientAddress };
|
||||
const data = {
|
||||
$unset: {
|
||||
|
|
@ -322,7 +322,7 @@ class UnknownUser {
|
|||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.update(query, data);
|
||||
await this.AccountsLockoutCollection.updateAsync(query, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ temporary rendered events).
|
|||
/* resizer (touch devices) */
|
||||
.fc-h-event.fc-selected .fc-resizer {
|
||||
/* 8x8 little dot */
|
||||
border-radius: 4px;
|
||||
border-radius: 0.4ch;
|
||||
border-width: 1px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
|
@ -1145,7 +1145,7 @@ be a descendant of the grid when it is being dragged.
|
|||
height: 8px;
|
||||
overflow: hidden;
|
||||
line-height: 8px;
|
||||
font-size: 11px;
|
||||
|
||||
font-family: monospace;
|
||||
text-align: center;
|
||||
cursor: s-resize; }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
// creates Object if not present in collection
|
||||
// initArr = [displayName, shortName, website, isActive]
|
||||
// objString = ["Org","Team"] for method mapping
|
||||
function createObject(initArr, objString)
|
||||
async function createObject(initArr, objString)
|
||||
{
|
||||
functionName = objString === "Org" ? 'setCreateOrgFromOidc' : 'setCreateTeamFromOidc';
|
||||
creationString = 'setCreate'+ objString + 'FromOidc';
|
||||
return Meteor.call(functionName,
|
||||
return await Meteor.callAsync(functionName,
|
||||
initArr[0],//displayName
|
||||
initArr[1],//desc
|
||||
initArr[2],//shortName
|
||||
|
|
@ -13,10 +13,10 @@ function createObject(initArr, objString)
|
|||
initArr[4]//xxxisActive
|
||||
);
|
||||
}
|
||||
function updateObject(initArr, objString)
|
||||
async function updateObject(initArr, objString)
|
||||
{
|
||||
functionName = objString === "Org" ? 'setOrgAllFieldsFromOidc' : 'setTeamAllFieldsFromOidc';
|
||||
return Meteor.call(functionName,
|
||||
return await Meteor.callAsync(functionName,
|
||||
initArr[0],//team || org Object
|
||||
initArr[1],//displayName
|
||||
initArr[2],//desc
|
||||
|
|
@ -57,7 +57,7 @@ module.exports = {
|
|||
// isAdmin: [true, false] -> admin group becomes admin in wekan
|
||||
// isOrganization: [true, false] -> creates org and adds to user
|
||||
// displayName: "string"
|
||||
addGroupsWithAttributes: function (user, groups){
|
||||
addGroupsWithAttributes: async function (user, groups){
|
||||
teamArray=[];
|
||||
orgArray=[];
|
||||
isAdmin = [];
|
||||
|
|
@ -76,20 +76,20 @@ addGroupsWithAttributes: function (user, groups){
|
|||
isAdmin.push(group.isAdmin || false);
|
||||
if (isOrg)
|
||||
{
|
||||
org = Org.findOne({"orgDisplayName": group.displayName});
|
||||
org = await Org.findOneAsync({"orgDisplayName": group.displayName});
|
||||
if(org)
|
||||
{
|
||||
if(contains(orgs, org, "org"))
|
||||
{
|
||||
initAttributes.unshift(org);
|
||||
updateObject(initAttributes, "Org");
|
||||
await updateObject(initAttributes, "Org");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if(forceCreate)
|
||||
{
|
||||
createObject(initAttributes, "Org");
|
||||
org = Org.findOne({'orgDisplayName': group.displayName});
|
||||
await createObject(initAttributes, "Org");
|
||||
org = await Org.findOneAsync({'orgDisplayName': group.displayName});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -102,20 +102,20 @@ addGroupsWithAttributes: function (user, groups){
|
|||
else
|
||||
{
|
||||
//start team routine
|
||||
team = Team.findOne({"teamDisplayName": group.displayName});
|
||||
team = await Team.findOneAsync({"teamDisplayName": group.displayName});
|
||||
if (team)
|
||||
{
|
||||
if(contains(teams, team, "team"))
|
||||
{
|
||||
initAttributes.unshift(team);
|
||||
updateObject(initAttributes, "Team");
|
||||
await updateObject(initAttributes, "Team");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if(forceCreate)
|
||||
{
|
||||
createObject(initAttributes, "Team");
|
||||
team = Team.findOne({'teamDisplayName': group.displayName});
|
||||
await createObject(initAttributes, "Team");
|
||||
team = await Team.findOneAsync({'teamDisplayName': group.displayName});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -129,28 +129,28 @@ addGroupsWithAttributes: function (user, groups){
|
|||
// hence user will get admin privileges in wekan
|
||||
// E.g. Admin rights will be withdrawn if no group in oidc provider has isAdmin set to true
|
||||
|
||||
users.update({ _id: user._id }, { $set: {isAdmin: isAdmin.some(i => (i === true))}});
|
||||
await users.updateAsync({ _id: user._id }, { $set: {isAdmin: isAdmin.some(i => (i === true))}});
|
||||
teams = {'teams': {'$each': teamArray}};
|
||||
orgs = {'orgs': {'$each': orgArray}};
|
||||
users.update({ _id: user._id }, { $push: teams});
|
||||
users.update({ _id: user._id }, { $push: orgs});
|
||||
await users.updateAsync({ _id: user._id }, { $push: teams});
|
||||
await users.updateAsync({ _id: user._id }, { $push: orgs});
|
||||
// remove temporary oidc data from user collection
|
||||
users.update({ _id: user._id }, { $unset: {"services.oidc.groups": []}});
|
||||
await users.updateAsync({ _id: user._id }, { $unset: {"services.oidc.groups": []}});
|
||||
|
||||
return;
|
||||
},
|
||||
|
||||
changeUsername: function(user, name)
|
||||
changeUsername: async function(user, name)
|
||||
{
|
||||
username = {'username': name};
|
||||
if (user.username != username) users.update({ _id: user._id }, { $set: username});
|
||||
if (user.username != username) await users.updateAsync({ _id: user._id }, { $set: username});
|
||||
},
|
||||
changeFullname: function(user, name)
|
||||
changeFullname: async function(user, name)
|
||||
{
|
||||
username = {'profile.fullname': name};
|
||||
if (user.username != username) users.update({ _id: user._id }, { $set: username});
|
||||
if (user.username != username) await users.updateAsync({ _id: user._id }, { $set: username});
|
||||
},
|
||||
addEmail: function(user, email)
|
||||
addEmail: async function(user, email)
|
||||
{
|
||||
user_email = user.emails || [];
|
||||
var contained = false;
|
||||
|
|
@ -173,7 +173,7 @@ addEmail: function(user, email)
|
|||
{
|
||||
user_email.unshift({'address': email, 'verified': true});
|
||||
user_email = {'emails': user_email};
|
||||
users.update({ _id: user._id }, { $set: user_email});
|
||||
await users.updateAsync({ _id: user._id }, { $set: user_email});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import {addGroupsWithAttributes, addEmail, changeFullname, changeUsername} from './loginHandler';
|
||||
import { fetch, Headers } from 'meteor/fetch';
|
||||
import { URLSearchParams } from 'meteor/url';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import https from 'https';
|
||||
import fs from 'fs';
|
||||
|
||||
Oidc = {};
|
||||
httpCa = false;
|
||||
|
||||
if (process.env.OAUTH2_CA_CERT !== undefined) {
|
||||
try {
|
||||
const fs = Npm.require('fs');
|
||||
if (fs.existsSync(process.env.OAUTH2_CA_CERT)) {
|
||||
httpCa = fs.readFileSync(process.env.OAUTH2_CA_CERT);
|
||||
}
|
||||
|
|
@ -18,10 +22,10 @@ var profile = {};
|
|||
var serviceData = {};
|
||||
var userinfo = {};
|
||||
|
||||
OAuth.registerService('oidc', 2, null, function (query) {
|
||||
OAuth.registerService('oidc', 2, null, async function (query) {
|
||||
var debug = process.env.DEBUG === 'true';
|
||||
|
||||
var token = getToken(query);
|
||||
var token = await getToken(query);
|
||||
if (debug) console.log('XXX: register token:', token);
|
||||
|
||||
var accessToken = token.access_token || token.id_token;
|
||||
|
|
@ -40,7 +44,7 @@ OAuth.registerService('oidc', 2, null, function (query) {
|
|||
else
|
||||
{
|
||||
// normal behaviour, getting the claims from UserInfo endpoint.
|
||||
userinfo = getUserInfo(accessToken);
|
||||
userinfo = await getUserInfo(accessToken);
|
||||
}
|
||||
|
||||
if (userinfo.ocs) userinfo = userinfo.ocs.data; // Nextcloud hack
|
||||
|
|
@ -73,7 +77,8 @@ OAuth.registerService('oidc', 2, null, function (query) {
|
|||
|
||||
if (accessToken) {
|
||||
var tokenContent = getTokenContent(accessToken);
|
||||
var fields = _.pick(tokenContent, getConfiguration().idTokenWhitelistFields);
|
||||
var config = await getConfiguration();
|
||||
var fields = _.pick(tokenContent, config.idTokenWhitelistFields);
|
||||
_.extend(serviceData, fields);
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +105,7 @@ OAuth.registerService('oidc', 2, null, function (query) {
|
|||
// therefore: keep admin privileges for wekan as before
|
||||
if(Array.isArray(serviceData.groups) && serviceData.groups.length && typeof serviceData.groups[0] === "string" )
|
||||
{
|
||||
user = Meteor.users.findOne({'_id': serviceData.id});
|
||||
user = await Meteor.users.findOneAsync({'_id': serviceData.id});
|
||||
|
||||
serviceData.groups.forEach(function(groupName, i)
|
||||
{
|
||||
|
|
@ -119,8 +124,8 @@ OAuth.registerService('oidc', 2, null, function (query) {
|
|||
|
||||
// Fix OIDC login loop for integer user ID. Thanks to danielkaiser.
|
||||
// https://github.com/wekan/wekan/issues/4795
|
||||
Meteor.call('groupRoutineOnLogin',serviceData, ""+serviceData.id);
|
||||
Meteor.call('boardRoutineOnLogin',serviceData, ""+serviceData.id);
|
||||
await Meteor.callAsync('groupRoutineOnLogin',serviceData, ""+serviceData.id);
|
||||
await Meteor.callAsync('boardRoutineOnLogin',serviceData, ""+serviceData.id);
|
||||
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
|
|
@ -134,143 +139,166 @@ if (Meteor.release) {
|
|||
}
|
||||
|
||||
if (process.env.ORACLE_OIM_ENABLED !== 'true' && process.env.ORACLE_OIM_ENABLED !== true) {
|
||||
var getToken = function (query) {
|
||||
var getToken = async function (query) {
|
||||
var debug = process.env.DEBUG === 'true';
|
||||
var config = getConfiguration();
|
||||
var config = await getConfiguration();
|
||||
var serverTokenEndpoint;
|
||||
if(config.tokenEndpoint.includes('https://')){
|
||||
var serverTokenEndpoint = config.tokenEndpoint;
|
||||
serverTokenEndpoint = config.tokenEndpoint;
|
||||
}else{
|
||||
var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
|
||||
serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
|
||||
}
|
||||
var requestPermissions = config.requestPermissions;
|
||||
var response;
|
||||
|
||||
try {
|
||||
var postOptions = {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
"User-Agent": userAgent
|
||||
},
|
||||
params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
redirect_uri: OAuth._redirectUri('oidc', config),
|
||||
grant_type: 'authorization_code',
|
||||
state: query.state
|
||||
}
|
||||
};
|
||||
var body = new URLSearchParams({
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
redirect_uri: OAuth._redirectUri('oidc', config),
|
||||
grant_type: 'authorization_code',
|
||||
state: query.state
|
||||
});
|
||||
|
||||
var fetchOptions = {
|
||||
method: 'POST',
|
||||
headers: new Headers({
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': userAgent,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}),
|
||||
body: body.toString()
|
||||
};
|
||||
|
||||
if (httpCa) {
|
||||
postOptions['npmRequestOptions'] = { ca: httpCa };
|
||||
fetchOptions.agent = new https.Agent({ ca: httpCa });
|
||||
}
|
||||
response = HTTP.post(serverTokenEndpoint, postOptions);
|
||||
|
||||
var response = await fetch(serverTokenEndpoint, fetchOptions);
|
||||
var data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to get token from OIDC " + serverTokenEndpoint + ": " + response.statusText);
|
||||
}
|
||||
if (data.error) {
|
||||
// if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + data.error);
|
||||
}
|
||||
if (debug) console.log('XXX: getToken response: ', data);
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw _.extend(new Error("Failed to get token from OIDC " + serverTokenEndpoint + ": " + err.message),
|
||||
{ response: err.response });
|
||||
}
|
||||
if (response.data.error) {
|
||||
// if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + response.data.error);
|
||||
} else {
|
||||
if (debug) console.log('XXX: getToken response: ', response.data);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.ORACLE_OIM_ENABLED === 'true' || process.env.ORACLE_OIM_ENABLED === true) {
|
||||
|
||||
var getToken = function (query) {
|
||||
var getToken = async function (query) {
|
||||
var debug = process.env.DEBUG === 'true';
|
||||
var config = getConfiguration();
|
||||
var config = await getConfiguration();
|
||||
var serverTokenEndpoint;
|
||||
if(config.tokenEndpoint.includes('https://')){
|
||||
var serverTokenEndpoint = config.tokenEndpoint;
|
||||
serverTokenEndpoint = config.tokenEndpoint;
|
||||
}else{
|
||||
var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
|
||||
serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
|
||||
}
|
||||
var requestPermissions = config.requestPermissions;
|
||||
var response;
|
||||
|
||||
// OIM needs basic Authentication token in the header - ClientID + SECRET in base64
|
||||
var dataToken=null;
|
||||
var strBasicToken=null;
|
||||
var strBasicToken64=null;
|
||||
|
||||
dataToken = process.env.OAUTH2_CLIENT_ID + ':' + process.env.OAUTH2_SECRET;
|
||||
strBasicToken = new Buffer(dataToken);
|
||||
strBasicToken64 = strBasicToken.toString('base64');
|
||||
var dataToken = process.env.OAUTH2_CLIENT_ID + ':' + process.env.OAUTH2_SECRET;
|
||||
var strBasicToken64 = Buffer.from(dataToken).toString('base64');
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
if (debug) console.log('Basic Token: ', strBasicToken64);
|
||||
|
||||
try {
|
||||
var postOptions = {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
"User-Agent": userAgent,
|
||||
"Authorization": "Basic " + strBasicToken64
|
||||
},
|
||||
params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
redirect_uri: OAuth._redirectUri('oidc', config),
|
||||
grant_type: 'authorization_code',
|
||||
state: query.state
|
||||
}
|
||||
};
|
||||
var body = new URLSearchParams({
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
redirect_uri: OAuth._redirectUri('oidc', config),
|
||||
grant_type: 'authorization_code',
|
||||
state: query.state
|
||||
});
|
||||
|
||||
var fetchOptions = {
|
||||
method: 'POST',
|
||||
headers: new Headers({
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': userAgent,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Basic ' + strBasicToken64
|
||||
}),
|
||||
body: body.toString()
|
||||
};
|
||||
|
||||
if (httpCa) {
|
||||
postOptions['npmRequestOptions'] = { ca: httpCa };
|
||||
fetchOptions.agent = new https.Agent({ ca: httpCa });
|
||||
}
|
||||
response = HTTP.post(serverTokenEndpoint, postOptions);
|
||||
|
||||
var response = await fetch(serverTokenEndpoint, fetchOptions);
|
||||
var data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to get token from OIDC " + serverTokenEndpoint + ": " + response.statusText);
|
||||
}
|
||||
if (data.error) {
|
||||
// if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + data.error);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
if (debug) console.log('XXX: getToken response: ', data);
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw _.extend(new Error("Failed to get token from OIDC " + serverTokenEndpoint + ": " + err.message),
|
||||
{ response: err.response });
|
||||
}
|
||||
if (response.data.error) {
|
||||
// if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + response.data.error);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
if (debug) console.log('XXX: getToken response: ', response.data);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
var getUserInfo = function (accessToken) {
|
||||
var getUserInfo = async function (accessToken) {
|
||||
var debug = process.env.DEBUG === 'true';
|
||||
var config = getConfiguration();
|
||||
var config = await getConfiguration();
|
||||
// Some userinfo endpoints use a different base URL than the authorization or token endpoints.
|
||||
// This logic allows the end user to override the setting by providing the full URL to userinfo in their config.
|
||||
var serverUserinfoEndpoint;
|
||||
if (config.userinfoEndpoint.includes("https://")) {
|
||||
var serverUserinfoEndpoint = config.userinfoEndpoint;
|
||||
serverUserinfoEndpoint = config.userinfoEndpoint;
|
||||
} else {
|
||||
var serverUserinfoEndpoint = config.serverUrl + config.userinfoEndpoint;
|
||||
serverUserinfoEndpoint = config.serverUrl + config.userinfoEndpoint;
|
||||
}
|
||||
var response;
|
||||
|
||||
try {
|
||||
var getOptions = {
|
||||
headers: {
|
||||
"User-Agent": userAgent,
|
||||
"Authorization": "Bearer " + accessToken
|
||||
}
|
||||
};
|
||||
var fetchOptions = {
|
||||
method: 'GET',
|
||||
headers: new Headers({
|
||||
'User-Agent': userAgent,
|
||||
'Authorization': 'Bearer ' + accessToken
|
||||
})
|
||||
};
|
||||
|
||||
if (httpCa) {
|
||||
getOptions['npmRequestOptions'] = { ca: httpCa };
|
||||
fetchOptions.agent = new https.Agent({ ca: httpCa });
|
||||
}
|
||||
response = HTTP.get(serverUserinfoEndpoint, getOptions);
|
||||
|
||||
var response = await fetch(serverUserinfoEndpoint, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch userinfo from OIDC " + serverUserinfoEndpoint + ": " + response.statusText);
|
||||
}
|
||||
|
||||
var data = await response.json();
|
||||
if (debug) console.log('XXX: getUserInfo response: ', data);
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw _.extend(new Error("Failed to fetch userinfo from OIDC " + serverUserinfoEndpoint + ": " + err.message),
|
||||
{response: err.response});
|
||||
}
|
||||
if (debug) console.log('XXX: getUserInfo response: ', response.data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
var getConfiguration = function () {
|
||||
var config = ServiceConfiguration.configurations.findOne({ service: 'oidc' });
|
||||
var getConfiguration = async function () {
|
||||
var config = await ServiceConfiguration.configurations.findOneAsync({ service: 'oidc' });
|
||||
if (!config) {
|
||||
throw new ServiceConfiguration.ConfigError('Service oidc not configured.');
|
||||
}
|
||||
|
|
@ -295,24 +323,24 @@ var getTokenContent = function (token) {
|
|||
return content;
|
||||
}
|
||||
Meteor.methods({
|
||||
'groupRoutineOnLogin': function(info, userId)
|
||||
'groupRoutineOnLogin': async function(info, userId)
|
||||
{
|
||||
check(info, Object);
|
||||
check(userId, String);
|
||||
var propagateOidcData = process.env.PROPAGATE_OIDC_DATA || false;
|
||||
if (propagateOidcData) {
|
||||
users= Meteor.users;
|
||||
user = users.findOne({'services.oidc.id': userId});
|
||||
user = await users.findOneAsync({'services.oidc.id': userId});
|
||||
|
||||
if(user) {
|
||||
//updates/creates Groups and user admin privileges accordingly if not undefined
|
||||
if (info.groups) {
|
||||
addGroupsWithAttributes(user, info.groups);
|
||||
await addGroupsWithAttributes(user, info.groups);
|
||||
}
|
||||
|
||||
if(info.email) addEmail(user, info.email);
|
||||
if(info.fullname) changeFullname(user, info.fullname);
|
||||
if(info.username) changeUsername(user, info.username);
|
||||
if(info.email) await addEmail(user, info.email);
|
||||
if(info.fullname) await changeFullname(user, info.fullname);
|
||||
if(info.username) await changeUsername(user, info.username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -328,8 +356,9 @@ Meteor.methods({
|
|||
const defaultBoardId = defaultBoardParams.shift()
|
||||
if (!defaultBoardId) return
|
||||
|
||||
const board = Boards.findOne(defaultBoardId)
|
||||
const userId = Users.findOne({ 'services.oidc.id': oidcUserId })?._id
|
||||
const board = await Boards.findOneAsync(defaultBoardId)
|
||||
const user = await Users.findOneAsync({ 'services.oidc.id': oidcUserId })
|
||||
const userId = user?._id
|
||||
const memberIndex = _.pluck(board?.members, 'userId').indexOf(userId);
|
||||
if(!board || !userId || memberIndex > -1) return
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Package.describe({
|
||||
summary: "OpenID Connect (OIDC) flow for Meteor",
|
||||
version: "1.0.12",
|
||||
version: "1.1.0",
|
||||
name: "wekan-oidc",
|
||||
git: "https://github.com/wekan/wekan-oidc.git",
|
||||
});
|
||||
|
|
@ -8,7 +8,7 @@ Package.describe({
|
|||
Package.onUse(function(api) {
|
||||
api.use('oauth2', ['client', 'server']);
|
||||
api.use('oauth', ['client', 'server']);
|
||||
api.use('http', ['server']);
|
||||
api.use('fetch', ['server']);
|
||||
api.use('underscore', 'client');
|
||||
api.use('ecmascript');
|
||||
api.use('templating', 'client');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue