From 5b60efbe8258150587ecece38f5ac32ff0c5bba9 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 4 Mar 2022 18:24:11 +0100 Subject: [PATCH 1/3] fix weird behavior where freshly created teams/orgs as active are crossed, recreated consistency with database --- client/components/settings/peopleBody.jade | 64 +++++++++++----------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/client/components/settings/peopleBody.jade b/client/components/settings/peopleBody.jade index f0b6da965..7ac86550d 100644 --- a/client/components/settings/peopleBody.jade +++ b/client/components/settings/peopleBody.jade @@ -148,30 +148,30 @@ template(name="newUserRow") template(name="orgRow") tr if orgData.orgIsActive - td {{ orgData.orgDisplayName }} - else td {{ orgData.orgDisplayName }} - if orgData.orgIsActive - td {{ orgData.orgDesc }} else + td {{ orgData.orgDisplayName }} + if orgData.orgIsActive td {{ orgData.orgDesc }} - if orgData.orgIsActive - td {{ orgData.orgShortName }} else + td {{ orgData.orgDesc }} + if orgData.orgIsActive td {{ orgData.orgShortName }} - if orgData.orgIsActive - td {{ orgData.orgWebsite }} else + td {{ orgData.orgShortName }} + if orgData.orgIsActive td {{ orgData.orgWebsite }} - if orgData.orgIsActive - td {{ moment orgData.createdAt 'LLL' }} else + td {{ orgData.orgWebsite }} + if orgData.orgIsActive td {{ moment orgData.createdAt 'LLL' }} + else + td {{ moment orgData.createdAt 'LLL' }} td if orgData.orgIsActive - | {{_ 'no'}} - else | {{_ 'yes'}} + else + | {{_ 'no'}} td a.edit-org i.fa.fa-edit @@ -182,30 +182,30 @@ template(name="orgRow") template(name="teamRow") tr if teamData.teamIsActive - td {{ teamData.teamDisplayName }} - else td {{ teamData.teamDisplayName }} - if teamData.teamIsActive - td {{ teamData.teamDesc }} else + td {{ teamData.teamDisplayName }} + if teamData.teamIsActive td {{ teamData.teamDesc }} - if teamData.teamIsActive - td {{ teamData.teamShortName }} else + td {{ teamData.teamDesc }} + if teamData.teamIsActive td {{ teamData.teamShortName }} - if teamData.teamIsActive - td {{ teamData.teamWebsite }} else + td {{ teamData.teamShortName }} + if teamData.teamIsActive td {{ teamData.teamWebsite }} - if teamData.teamIsActive - td {{ moment teamData.createdAt 'LLL' }} else + td {{ teamData.teamWebsite }} + if teamData.teamIsActive td {{ moment teamData.createdAt 'LLL' }} + else + td {{ moment teamData.createdAt 'LLL' }} td if teamData.teamIsActive - | {{_ 'no'}} - else | {{_ 'yes'}} + else + | {{_ 'no'}} td a.edit-team i.fa.fa-edit @@ -313,8 +313,8 @@ template(name="editOrgPopup") label | {{_ 'active'}} select.select-active.js-org-isactive - option(value="false") {{_ 'yes'}} - option(value="true" selected="{{org.orgIsActive}}") {{_ 'no'}} + option(value="false") {{_ 'no'}} + option(value="true" selected="{{org.orgIsActive}}") {{_ 'yes'}} hr div.buttonsContainer input.primary.wide(type="submit" value="{{_ 'save'}}") @@ -339,8 +339,8 @@ template(name="editTeamPopup") label | {{_ 'active'}} select.select-active.js-team-isactive - option(value="false") {{_ 'yes'}} - option(value="true" selected="{{team.teamIsActive}}") {{_ 'no'}} + option(value="false") {{_ 'no'}} + option(value="true" selected="{{team.teamIsActive}}") {{_ 'yes'}} hr div.buttonsContainer input.primary.wide(type="submit" value="{{_ 'save'}}") @@ -442,8 +442,8 @@ template(name="newOrgPopup") label | {{_ 'active'}} select.select-active.js-org-isactive - option(value="false" selected="selected") {{_ 'yes'}} - option(value="true") {{_ 'no'}} + option(value="false" selected="selected") {{_ 'no'}} + option(value="true") {{_ 'yes'}} hr div.buttonsContainer input.primary.wide(type="submit" value="{{_ 'save'}}") @@ -466,8 +466,8 @@ template(name="newTeamPopup") label | {{_ 'active'}} select.select-active.js-team-isactive - option(value="false" selected="selected") {{_ 'yes'}} - option(value="true") {{_ 'no'}} + option(value="false" selected="selected") {{_ 'no'}} + option(value="true") {{_ 'yes'}} hr div.buttonsContainer input.primary.wide(type="submit" value="{{_ 'save'}}") From a0dbfa1f7ec0384f88d62392c2b3e0cf3ce9a0a6 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 4 Mar 2022 18:29:29 +0100 Subject: [PATCH 2/3] add README and functionality for more control through oidc - create teams/orgs if not exist and addto user - make user admin when flag is set --- models/org.js | 25 ++++ models/team.js | 26 ++++- packages/wekan-oidc/README.md | 41 +++++++ packages/wekan-oidc/loginHandler.js | 175 ++++++++++++++++++++++------ packages/wekan-oidc/oidc_server.js | 14 +-- 5 files changed, 234 insertions(+), 47 deletions(-) diff --git a/models/org.js b/models/org.js index 14c8b0440..93631e06b 100644 --- a/models/org.js +++ b/models/org.js @@ -143,7 +143,32 @@ if (Meteor.isServer) { } } }, + setCreateOrgFromOidc( + orgDisplayName, + orgDesc, + orgShortName, + orgWebsite, + orgIsActive, + ) { + check(orgDisplayName, String); + check(orgDesc, String); + check(orgShortName, String); + check(orgWebsite, String); + check(orgIsActive, Boolean); + const nOrgNames = Org.find({ orgShortName }).count(); + if (nOrgNames > 0) { + throw new Meteor.Error('orgname-already-taken'); + } else { + Org.insert({ + orgDisplayName, + orgDesc, + orgShortName, + orgWebsite, + orgIsActive, + }); + } + }, setOrgDisplayName(org, orgDisplayName) { if (Meteor.user() && Meteor.user().isAdmin) { check(org, Object); diff --git a/models/team.js b/models/team.js index 18ce8574c..5d568bc9b 100644 --- a/models/team.js +++ b/models/team.js @@ -141,7 +141,31 @@ if (Meteor.isServer) { } } }, - + setCreateTeamFromOidc( + teamDisplayName, + teamDesc, + teamShortName, + teamWebsite, + teamIsActive, + ) { + check(teamDisplayName, String); + check(teamDesc, String); + check(teamShortName, String); + check(teamWebsite, String); + check(teamIsActive, Boolean); + const nTeamNames = Team.find({ teamShortName }).count(); + if (nTeamNames > 0) { + throw new Meteor.Error('teamname-already-taken'); + } else { + Team.insert({ + teamDisplayName, + teamDesc, + teamShortName, + teamWebsite, + teamIsActive, + }); + } + }, setTeamDisplayName(team, teamDisplayName) { if (Meteor.user() && Meteor.user().isAdmin) { check(team, Object); diff --git a/packages/wekan-oidc/README.md b/packages/wekan-oidc/README.md index 8948971c0..3201869cd 100644 --- a/packages/wekan-oidc/README.md +++ b/packages/wekan-oidc/README.md @@ -5,3 +5,44 @@ A Meteor implementation of OpenID Connect Login flow ## Usage and Documentation Look at the `salleman:accounts-oidc` package for the documentation about using OpenID Connect with Meteor. + +## Usage with e.g. authentik for updating users via oidc + +To use the following features set: +'export PROPAGATE_OIDC_DATA=true' + +SIMPLE: If user is assigned to 'group in authentik' it will be automatically assigned to corresponding team in wekan if exists + +ADVANCED: Users can be assigned to teams or organisations via oidc on login. Teams and organisations that do not exist in wekan, yet, will be created, when specified. Admin privileges for wekan through a specific group can be set via Oidc. +See example below: + + + 1. Specify scope in authentik for what will be delivered via userinfo["wekanGroups"] + + Possible configuration for *yourScope*: + ' + groupsDict = {"wekanGroups": []} + for group in request.user.ak_groups.all(): + groupDict = {"displayName": group.name} + groupAdmin = {"isAdmin": group.isAdmin} + groupAttributes = group.attributes + tmp_dict= groupDict | groupAttributes | groupAdmin + + groupsDict["wekanGroups"].append(tmp_dict) + return groupsDict + ' + 2. Tell provider to include *yourScope* and set + OAUTH2_REQUEST_PERMISSIONS="openid profile email *yourScope*" + + 3. In your group settings in authentik add attributes: + desc: groupDesc // default group.name + isAdmin: "true" // default "false" + website: groupWebsite // default group.name + isActive: "true" // default "false" + shortName: groupShortname // default group.name + forceCreate: "true" // default "false" + isOrganisation: "true" // default "false" + + 4. On next login user will be added to either newly created group/organization or to already existing + + NOTE: orgs & teams won't be updated if they already exist. diff --git a/packages/wekan-oidc/loginHandler.js b/packages/wekan-oidc/loginHandler.js index 2107b6e43..8526a4e14 100644 --- a/packages/wekan-oidc/loginHandler.js +++ b/packages/wekan-oidc/loginHandler.js @@ -1,53 +1,153 @@ -module.exports = { - addGroups: function (user, groups){ - teamArray=[] - teams = user.teams - if (!teams) +// creates Object if not present in collection +// initArr = [displayName, shortName, website, isActive] +// objString = ["Org","Team"] for method mapping +function createObject(initArr, objString) +{ + functionName = objString === "Org" ? 'setCreateOrgFromOidc' : 'setCreateTeamFromOidc'; + creationString = 'setCreate'+ objString + 'FromOidc'; + return Meteor.call(functionName, + initArr[0],//displayName + initArr[1],//desc + initArr[2],//shortName + initArr[3],//website + initArr[4]//xxxisActive + ); +} +//checks whether obj is in collection of userObjs +//params +//e.g. userObjs = user.teams +//e.g. obj = Team.findOne... +//e.g. collection = "team" +function contains(userObjs, obj, collection) +{ + id = collection+'Id'; + + if(!userObjs.length) { - for (group of groups){ - team = Team.findOne({"teamDisplayName": group}); - if (team) + return false; + } + for (const [count, hash] of Object.entries(userObjs)) + { + if (hash[id] === obj._id) { - team_hash = {'teamId': team._id, 'teamDisplayName': group} - teamArray.push(team_hash); + return true; } } - teams = {'teams': teamArray} - users.update({ _id: user._id }, { $set: teams}); - return; - } - else{ - - for (group of groups){ - team = Team.findOne({"teamDisplayName": group}) - team_contained= false; - if (team) - { - team_hash = {'teamId': team._id, 'teamDisplayName': group} - for (const [count,teams_hash] of Object.entries(teams)) - { - if (teams_hash["teamId"] === team._id) - { - team_contained=true; - break; - } - } - if (team_contained) + return false; +} +module.exports = { + // Soft version of adding teams to user via Oidc + // teams won't be created if nonexistent + // groups are treated as teams in the general case + addGroups: function (user, groups){ + teamArray=[]; + teams = user.teams; + orgArray=[]; + for (group of groups){ + team = Team.findOne({"teamDisplayName": group}); + if(team) + { + if (contains(teams,team,"team")) { continue; } else { - console.log("TEAM to be added:", team); - teams.push({'teamId': Team.findOne({'teamDisplayName': group})._id, 'teamDisplayName': group}); + teamArray.push({'teamId': Team.findOne({'teamDisplayName': group})._id, 'teamDisplayName': group}); } } } - console.log("XXXXXXXXXXX Team Array: ", teams); - teams = {'teams': teams} - users.update({ _id: user._id }, { $set: teams}); - } + teams = {'teams': { '$each': teamArray}}; + users.update({ _id: user._id }, { $push: teams}); }, + +// This function adds groups as organizations or teams to users and +// creates them if not already existing +// DEFAULT after creation orgIsActive & teamIsActive: true +// PODC provider needs to send group data within "wekanGroup" scope +// PARAMS to be set for groups within your Oidc provider: +// isAdmin: [true, false] -> admin group becomes admin in wekan +// isOrganization: [true, false] -> creates org and adds to user +// displayName: "string" +addGroupsWithAttributes: function (user, groups){ + teamArray=[]; + orgArray=[]; + teams = user.teams; + orgs = user.orgs; + for (group of groups) + { + isOrg = group.isOrganisation || false; + forceCreate = group.forceCreate|| false; + if (isOrg) + { + org = Org.findOne({"orgDisplayName": group.displayName}); + if(org) + { + if(contains(orgs, org, "org")) + { + continue; + } + } + else if(forceCreate) + { + initAttributes = [ + group.displayName, + group.desc || group.displayName, + group.shortName ||group.displayName, + group.website || group.displayName, group.isActive || false] + createObject(initAttributes, "Org"); + org = Org.findOne({'orgDisplayName': group.displayName}); + } + else + { + continue; + } + orgHash = {'orgId': org._id, 'orgDisplayName': group.displayName}; + orgArray.push(orgHash); + } + + else + { + //start team routine + team = Team.findOne({"teamDisplayName": group.displayName}); + if (team) + { + if(contains(teams, team, "team")) + { + continue; + } + } + else if(forceCreate) + { + initAttributes = [ + group.displayName, + group.desc || group.displayName, + group.shortName ||group.displayName, + group.website || group.displayName, + group.isActive || false] + createObject(initAttributes, "Team"); + team = Team.findOne({'teamDisplayName': group.displayName}); + } + else + { + continue; + } + teamHash = {'teamId': team._id, 'teamDisplayName': group.displayName}; + teamArray.push(teamHash); + } + // user is assigned to group which has set isAdmin: true in oidc data + // hence user will get admin privileges in wekan + if(group.isAdmin){ + users.update({ _id: user._id }, { $set: {isAdmin: true}}); + } + } + teams = {'teams': {'$each': teamArray}}; + orgs = {'orgs': {'$each': orgArray}}; + users.update({ _id: user._id }, { $push: teams}); + users.update({ _id: user._id }, { $push: orgs}); + return; +}, + changeUsername: function(user, name) { username = {'username': name}; @@ -81,7 +181,6 @@ addEmail: function(user, email) { user_email.unshift({'address': email, 'verified': true}); user_email = {'emails': user_email}; - console.log(user_email); users.update({ _id: user._id }, { $set: user_email}); } } diff --git a/packages/wekan-oidc/oidc_server.js b/packages/wekan-oidc/oidc_server.js index 27a4da3cc..23984b4ac 100644 --- a/packages/wekan-oidc/oidc_server.js +++ b/packages/wekan-oidc/oidc_server.js @@ -1,4 +1,4 @@ -import {addGroups, addEmail,changeFullname, changeUsername} from './loginHandler'; +import {addGroups, addGroupsWithAttributes, addEmail, changeFullname, changeUsername} from './loginHandler'; Oidc = {}; httpCa = false; @@ -18,7 +18,6 @@ if (process.env.OAUTH2_CA_CERT !== undefined) { OAuth.registerService('oidc', 2, null, function (query) { var debug = process.env.DEBUG || false; - console.log(process.env); var propagateOidcData = process.env.PROPAGATE_OIDC_DATA || false; var token = getToken(query); @@ -80,16 +79,15 @@ OAuth.registerService('oidc', 2, null, function (query) { profile.email = userinfo[process.env.OAUTH2_EMAIL_MAP]; // || userinfo["email"]; if (propagateOidcData) { + users= Meteor.users; user = users.findOne({'services.oidc.id': serviceData.id}); if(user) { - serviceData.groups = profile.groups - profile.groups = userinfo["groups"]; - if(userinfo["groups"]) addGroups(user, userinfo["groups"]); - if(profile.email) addEmail(user, profile.email) - if(profile.name) changeFullname(user, profile.name) - if(profile.username) changeUsername(user, profile.username) + (!userinfo?.["wekanGroups"]?.length) ? addGroups(user, userinfo["groups"]): addGroupsWithAttributes(user, userinfo["wekanGroups"]); + if(profile.email) addEmail(user, profile.email); + if(profile.name) changeFullname(user, profile.name); + if(profile.username) changeUsername(user, profile.username); } } if (debug) console.log('XXX: profile:', profile); From 263b405fc8e63d67dd5c7029e4ed52947c50ec85 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 4 Mar 2022 18:53:43 +0100 Subject: [PATCH 3/3] minor changes to README --- packages/wekan-oidc/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/wekan-oidc/README.md b/packages/wekan-oidc/README.md index 3201869cd..37344f96a 100644 --- a/packages/wekan-oidc/README.md +++ b/packages/wekan-oidc/README.md @@ -36,12 +36,12 @@ See example below: 3. In your group settings in authentik add attributes: desc: groupDesc // default group.name - isAdmin: "true" // default "false" + isAdmin: true // default false website: groupWebsite // default group.name - isActive: "true" // default "false" + isActive: true // default false shortName: groupShortname // default group.name - forceCreate: "true" // default "false" - isOrganisation: "true" // default "false" + forceCreate: true // default false + isOrganisation: true // default false 4. On next login user will be added to either newly created group/organization or to already existing