Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions backend/access/sourceAccess.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use strict';

const normalizeIds = (values) => {
if (!Array.isArray(values)) return [];

return values
.filter(Boolean)
.map((value) => {
if (value._id) return String(value._id);
return String(value);
});
};

const getUserTeamIds = (user) => {
if (!user || !Array.isArray(user.teams)) return [];
return normalizeIds(user.teams);
};

const getSourcePolicy = (source) => {
return source && source.accessPolicy
? source.accessPolicy
: {
mode: 'public',
teams: [],
cutoffDate: null,
};
};

const hasTeamOverlap = (userTeamIds, allowedTeamIds) => {
const userTeams = new Set(userTeamIds);
return allowedTeamIds.some((teamId) => userTeams.has(teamId));
};

const isAdmin = (user) => {
return user && user.role === 'admin';
};

const canAccessRestrictedPolicy = (user, policy) => {
if (isAdmin(user)) return true;

const userTeamIds = getUserTeamIds(user);
const allowedTeamIds = normalizeIds(policy.teams);

if (allowedTeamIds.length === 0) return false;

return hasTeamOverlap(userTeamIds, allowedTeamIds);
};

const canViewSource = (user, source) => {
if (isAdmin(user)) return true;

const policy = getSourcePolicy(source);

if (!policy.mode || policy.mode === 'public') {
return true;
}

if (policy.mode === 'restricted') {
return canAccessRestrictedPolicy(user, policy);
}

if (policy.mode === 'public_until') {
return canAccessRestrictedPolicy(user, policy);
}

return false;
};

const canViewSourceDataForDate = (user, source, recordDate) => {
if (isAdmin(user)) return true;

const policy = getSourcePolicy(source);

if (!policy.mode || policy.mode === 'public') {
return true;
}

if (policy.mode === 'restricted') {
return canAccessRestrictedPolicy(user, policy);
}

if (policy.mode === 'public_until') {
if (!policy.cutoffDate) {
return canAccessRestrictedPolicy(user, policy);
}

if (!recordDate) {
return canAccessRestrictedPolicy(user, policy);
}

const cutoffDate = new Date(policy.cutoffDate);
const itemDate = new Date(recordDate);

if (itemDate < cutoffDate) {
return true;
}

return canAccessRestrictedPolicy(user, policy);
}

return false;
};

module.exports = {
canViewSource,
canViewSourceDataForDate,
getSourcePolicy,
getUserTeamIds,
normalizeIds,
};
54 changes: 44 additions & 10 deletions backend/api/controllers/sourceController.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,33 @@
var Source = require('../../models/source');
var _ = require('lodash');

var sourcePopulate = [
{ path: 'user', select: 'username' },
{ path: 'credentials' },
{ path: 'accessPolicy.teams', select: 'name description active' },
];

// Create a new Source
exports.source_create = (req, res) => {
// set user as the logged in user
if (req.user) req.body.user = req.user._id;

normalizeAccessPolicy(req.body);

Source.create(req.body, function (err, source) {
if (err) {
return res.status(err.status).send(err.message);
}

res.status(200).send(source);
});
}

// Get a list of all sources
exports.source_sources = (req, res) => {
// Find all, exclude `events` field, populate user
Source.find({}, '-events', { sort: 'nickname' })
.populate([
{ path: 'user', select: 'username' },
{ path: 'credentials' }
])
Source.find({}, '-events', { sort: 'nickname' })
.populate(sourcePopulate)
.exec(function (err, sources) {
if (err) res.status(err.status).send(err.message);
else res.status(200).send(sources);
Expand All @@ -37,24 +43,52 @@ exports.source_details = (req, res) => {
else if (!source) return res.sendStatus(404);
Source.populate(
source,
[
{ path: 'user', select: 'username' },
{ path: 'credentials' }
],
sourcePopulate,
function (err, source) {
if (err) res.status(err.status).send(err.message);
else res.status(200).send(source);
});
});
}

//helper for source.lpopulate
var normalizeAccessPolicy = function (sourceData) {
if (!sourceData.accessPolicy) return;

var accessPolicy = sourceData.accessPolicy;

if (!accessPolicy.mode) {
accessPolicy.mode = 'public';
}

if (!Array.isArray(accessPolicy.teams)) {
accessPolicy.teams = [];
}

if (accessPolicy.cutoffDate === '') {
accessPolicy.cutoffDate = null;
}

if (accessPolicy.mode === 'public') {
accessPolicy.teams = [];
accessPolicy.cutoffDate = null;
}

if (accessPolicy.mode === 'restricted') {
accessPolicy.cutoffDate = null;
}
};


exports.source_update = (req, res, next) => {
if (req.params._id === '_events') return next();
// Find source to update
Source.findById(req.params._id, function (err, source) {
if (err) return res.status(err.status).send(err.message);
if (!source) return res.sendStatus(404);

normalizeAccessPolicy(req.body);

// Update the actual values
_.forEach(_.omit(req.body, ['_id', 'user', 'events']), function (val, key) {
source[key] = val;
Expand Down
137 changes: 130 additions & 7 deletions backend/api/controllers/teamController.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@
const User = require('../../models/user');
const Team = require('../../models/team');

const assignableRoles = ['viewer', 'monitor', 'team_lead'];

const canManageTeams = (user) => {
return user && ['admin', 'team_lead'].includes(user.role);
};

// Get all teams
exports.team_list = (req, res) => {
if (!req.user) return res.status(401).send('Unauthenticated.');

if (!canManageTeams(req.user)) {
return res.status(403).send('Unauthorized to view teams.');
}

Team.find({})
.sort({ name: 1 })
.lean()
Expand Down Expand Up @@ -61,10 +71,9 @@ exports.team_manageable_list = async (req, res) => {
exports.team_detail = async (req, res) => {
if (!req.user) return res.status(401).send('Unauthenticated.');

if (req.user.role !== 'admin') {
return res.status(403).send('Unauthorized to view team details.');
}

if (!canManageTeams(req.user)) {
return res.status(403).send('Unauthorized to view team details.');
}
try {
const team = await Team.findById(req.params._id)
.lean();
Expand Down Expand Up @@ -97,6 +106,10 @@ exports.team_detail = async (req, res) => {
exports.team_create = (req, res) => {
if (!req.user) return res.status(401).send('Unauthenticated.');

if (!canManageTeams(req.user)) {
return res.status(403).send('Unauthorized to create teams.');
}

const payload = {
name: req.body.name,
description: req.body.description || '',
Expand All @@ -114,13 +127,123 @@ exports.team_create = (req, res) => {
});
};

// Add or update a user's membership in a team
exports.team_add_member = async (req, res) => {
if (!req.user) return res.status(401).send('Unauthenticated.');

if (!canManageTeams(req.user)) {
return res.status(403).send('Unauthorized to manage team members.');
}

const userId = req.body.userId;
const role = req.body.role;

if (!userId) {
return res.status(400).send('Please provide a userId.');
}

if (!assignableRoles.includes(role)) {
return res.status(400).send('Role must be viewer, monitor, or team_lead.');
}

try {
const team = await Team.findById(req.params._id).lean();

if (!team) {
return res.sendStatus(404);
}

const user = await User.findById(userId).select('-password');

if (!user) {
return res.status(404).send('User not found.');
}

if (user.role === 'admin') {
return res.status(403).send('Admin users cannot be assigned from the team page.');
}

user.role = role;
user.teams = user.teams || [];

const alreadyInTeam = user.teams.some(
(teamId) => String(teamId) === String(req.params._id)
);

if (!alreadyInTeam) {
user.teams.push(req.params._id);
}

await user.save();

const members = await User.find({ teams: req.params._id })
.select('_id username displayName email role createdBy')
.sort({ role: 1, username: 1 })
.lean();

return res.status(200).send({
team,
members,
});
} catch (err) {
return res
.status(err.status || 500)
.send(err.message || 'Team member update failed');
}
};

// Remove a user from a team
exports.team_remove_member = async (req, res) => {
if (!req.user) return res.status(401).send('Unauthenticated.');

if (!canManageTeams(req.user)) {
return res.status(403).send('Unauthorized to manage team members.');
}

try {
const team = await Team.findById(req.params._id).lean();

if (!team) {
return res.sendStatus(404);
}

const user = await User.findById(req.params.userId).select('-password');

if (!user) {
return res.status(404).send('User not found.');
}

if (user.role === 'admin') {
return res.status(403).send('Admin users cannot be removed from teams here.');
}

await User.findByIdAndUpdate(req.params.userId, {
$pull: { teams: req.params._id },
});

const members = await User.find({ teams: req.params._id })
.select('_id username displayName email role createdBy')
.sort({ role: 1, username: 1 })
.lean();

return res.status(200).send({
team,
members,
});
} catch (err) {
return res
.status(err.status || 500)
.send(err.message || 'Team member removal failed');
}
};

// Delete a team
exports.team_delete = async (req, res) => {
if (!req.user) return res.status(401).send('Unauthenticated.');

if (req.user.role !== 'admin') {
return res.status(403).send('Unauthorized to delete teams.');
}
if (!canManageTeams(req.user)) {
return res.status(403).send('Unauthorized to delete teams.');
}

try {
const team = await Team.findById(req.params._id).lean();
Expand Down
Loading