Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
231c86c
Update permission checks to handle WebAPI 3.0 changes.
chrisknoll Mar 16, 2026
cf1a863
Modified authentication calls to handle anonymous users.
chrisknoll Mar 25, 2026
997d4e4
Fixed permission checks for characterization and feature analysis.
chrisknoll Apr 2, 2026
f708db2
Update permission checks for IR.
chrisknoll Apr 6, 2026
a160217
Fixed permissions for data-sources.
chrisknoll Apr 7, 2026
e0f9a9b
Remove preduction and estimation UI elements.
chrisknoll Apr 7, 2026
e5ac15c
Update permission checks for user management, tools, tags and source …
chrisknoll Apr 9, 2026
3f46c63
Cleanup app-init logic to dispose expired tokens.
chrisknoll Apr 15, 2026
896ad30
Remove priority check logic on source delete.
chrisknoll Apr 16, 2026
e86a14a
Added protected tag permission checks.
chrisknoll Apr 17, 2026
8388ec3
Disable conceptset quickview.
chrisknoll Apr 20, 2026
e0299af
Fixed logic when creating a new tag for a group directly in modal.
chrisknoll Apr 20, 2026
a66f7a2
Use pureComputed when doing simple calculations.
chrisknoll Apr 21, 2026
1c631a3
Add permission fetch to route change and poll interval.
chrisknoll Apr 21, 2026
32d1d2b
Remove Heracles reports from Cohort Definition.
chrisknoll Apr 23, 2026
41ca87f
Refactored cohort report into inclusion and demographics reports.
chrisknoll Apr 24, 2026
5eaf063
Cleanup isAuthenticated() calls.
chrisknoll Apr 29, 2026
cd9e5f3
Pass correct modeId to inclusion rule report.
chrisknoll May 1, 2026
bdf572b
Remove check that restricts generation to write-access to asset. Onl…
chrisknoll May 14, 2026
6796495
Use affirm logic when checking running state.
chrisknoll May 15, 2026
6d94ebd
Coerce withDemographics param in generate to boolean.
chrisknoll May 26, 2026
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
75 changes: 15 additions & 60 deletions js/Application.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ define(
|| sharedState.CohortDefinition.dirtyFlag().isDirty()
|| sharedState.IRAnalysis.dirtyFlag().isDirty()
|| sharedState.CohortPathways.dirtyFlag().isDirty()
|| sharedState.estimationAnalysis.dirtyFlag().isDirty()
|| sharedState.predictionAnalysis.dirtyFlag().isDirty()
|| sharedState.CohortCharacterization.dirtyFlag().isDirty()
);
});
Expand Down Expand Up @@ -118,45 +116,26 @@ define(
httpService.setUnauthorizedHandler(() => authApi.resetAuthParams());
httpService.setUserTokenGetter(() => authApi.getAuthorizationHeader());

const exp = authApi.tokenExpirationDate();
const now = new Date();

if (!exp || exp <= now) { authApi.resetAuthParams(); } else { await authApi.refreshToken(); }

try{
await authApi.loadUserInfo();
await i18nService.getAvailableLocales();
} catch (e) {
reject(e.message);
}

if (config.userAuthenticationEnabled) {
try {
// Routes to welcome are part of auth flow, loadUserInfo in this case is unnecessary and fails.
// More importantly it can trigger an infinite loop when skipLoginEnabled is enabled.
if (!window.location.href.includes("/welcome/")) {
await authApi.loadUserInfo();
}
} catch (e) {
reject(e.message);
}

}
authApi.isAuthenticated.subscribe(executionService.checkExecutionEngineStatus);
this.attachGlobalEventListeners();
await executionService.checkExecutionEngineStatus(authApi.isAuthenticated());

// Add user interaction listener that keeps refreshing the token as long
// as the user is active (either moving mouse, navigating with keyboard and/or typing):
var userInteractionCount = 0;
console.log("Adding user interaction listeners...");
// Add user interaction listeners to reset idle timeout
// (throttled inside resetIdleTimeout to at most once per minute)
["mouseover", "keydown", "focusin"].forEach(eventType => {
window.addEventListener(eventType, (event) => {
userInteractionCount++;
if (userInteractionCount % 30 == 0) {
console.log(">>> Checking user token....");
userInteractionCount = 0;
// Refresh the Atlas token if it is close to expiring:
if (authApi.isAuthenticated() && this.timeToExpire() < config.refreshTokenThreshold) {
console.log(">>> Token close to expiring. Refreshing user token....");
authApi.refreshToken();
}
}
});
window.addEventListener(eventType, () => authApi.resetIdleTimeout());
});

resolve();
Expand Down Expand Up @@ -198,39 +177,15 @@ define(
initServiceInformation() {
console.info('Initializing service information');
return new Promise((resolve, reject) => {
const serviceCacheKey = 'ATLAS|' + config.api.url;
const cachedService = lscache.get(serviceCacheKey);

if (cachedService && cachedService.sources) {
console.info('cached service');
config.api.sources = cachedService;
sourceApi.setSharedStateSources(cachedService.sources);
resolve();
} else {
sharedState.sources([]);

if (config.userAuthenticationEnabled && !authApi.isAuthenticated()) {
this.authSubscription = authApi.isAuthenticated.subscribe(async (isAuthed) => {
if (isAuthed) {
sharedState.appInitializationStatus(await sourceApi.initSourcesConfig());
this.authSubscription.dispose();
console.info('Re-initialized service information');
}
});
sharedState.appInitializationStatus(constants.applicationStatuses.running);
sourceApi.initSourcesConfig()
.then(function (appStatus) {
sharedState.appInitializationStatus(appStatus);
console.info('Init sources from server');
resolve();
return;
} else {
sourceApi.initSourcesConfig()
.then(function (appStatus) {
sharedState.appInitializationStatus(appStatus);
console.info('Init sources from server');
resolve();
});
}
}
});
});
}

checkOAuthError() {
let hash = window.location.hash;
if (hash && hash.includes("oauth_error_email")) {
Expand Down
8 changes: 3 additions & 5 deletions js/components/ac-access-denied.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
<div data-bind="css: classes(), visible: !isAuthenticated || !isPermitted()">
<!-- ko if: isAuthenticated && !isPermitted() -->
<div data-bind="css: classes(), visible: !isPermitted()">
<!-- ko if: message --><span data-bind="text: message"></span> <!-- /ko -->
<!-- ko if: !isPermitted() -->
<forbidden />
<!-- /ko -->
<!-- ko ifnot: isAuthenticated -->
<unauthenticated />
<!-- /ko -->
</div>
2 changes: 1 addition & 1 deletion js/components/ac-access-denied.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ define([
class AccessDenied extends Component {
constructor(params) {
super(params);
this.isAuthenticated = params.isAuthenticated;
this.message = params.message;
this.isPermitted = params.isPermitted || (() => false);
}
}
Expand Down
20 changes: 1 addition & 19 deletions js/components/atlas-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ define(['knockout', 'lscache', 'services/job/jobDetail', 'assets/ohdsi.util', 'c
state.currentVocabularyVersion(state.defaultVocabularyVersion());
}

state.sourceKeyOfVocabUrl = ko.computed(() => {
state.sourceKeyOfVocabUrl = ko.pureComputed(() => {
return state.vocabularyUrl() ? state.vocabularyUrl().replace(/\/$/, '').split('/').pop() : null;
});

Expand Down Expand Up @@ -84,24 +84,6 @@ define(['knockout', 'lscache', 'services/job/jobDetail', 'assets/ohdsi.util', 'c
};
state.CohortPathways.dirtyFlag = ko.observable(new ohdsiUtil.dirtyFlag(state.CohortPathways.current()));


state.estimationAnalysis = {
current: ko.observable(null),
analysisPath: null,
selectedId: ko.observable(null),
comparisons: ko.observableArray(),
}
state.estimationAnalysis.dirtyFlag = ko.observable(new ohdsiUtil.dirtyFlag(state.estimationAnalysis.current()));

state.predictionAnalysis = {
current: ko.observable(null),
analysisPath: null,
selectedId: ko.observable(null),
targetCohorts: ko.observableArray(),
outcomeCohorts: ko.observableArray(),
}
state.predictionAnalysis.dirtyFlag = ko.observable(new ohdsiUtil.dirtyFlag(state.predictionAnalysis.current()));

state.availableLocales = ko.observableArray();
state.locale = ko.observable();
state.localeSettings = ko.observable();
Expand Down
2 changes: 1 addition & 1 deletion js/components/atlas.cohort-editor.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="paddedWrapper">
<div class="paddedWrapper" data-bind="if: $component.currentCohortDefinition">
<!-- contenteditableSwitch must be placed AFTER contentEditable to override 'contenteditable' attribute -->
<div class="divtext cohort-description" data-bind="contentEditable: currentCohortDefinition().description, contenteditableSwitch: canEdit(), placeholder: ko.i18n('components.atlasCohortEditor.enterCohortPlaceholder', 'Enter the cohort definition description here')"></div>
<div data-bind="eventListener: [
Expand Down
4 changes: 2 additions & 2 deletions js/components/circe/components/ConceptSetBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ define([

self.isAuthenticated = authApi.isAuthenticated;
self.canReadConceptsets = ko.pureComputed(function () {
return (appConfig.userAuthenticationEnabled && self.isAuthenticated() && authApi.isPermittedReadConceptsets()) || !appConfig.userAuthenticationEnabled;
return true; // TODO: do not need permission to list entities
});
self.canReadCohorts = ko.pureComputed(function () {
return (config.userAuthenticationEnabled && self.isAuthenticated() && authApi.isPermittedReadCohorts()) || !config.userAuthenticationEnabled;
return true; // TODO: do not need permissions to list entities
});

self.loadConceptSetsFromRepository = function (url) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,6 @@
<span data-bind="text:ko.i18n('components.conceptSetSelector.clearConceptSet','Clear Concept Set')"></span>
</a></li>
</ul>
<ul>
<!-- ko if: previewVisible -->
<div onclick="event.stopPropagation();" class="undraggable"
style="border: solid 1px black;padding:5px; z-index: 999; background-color:white; position: absolute; max-height:200px; overflow-y:auto"
data-bind="style: { top: `${previewTop()}px`, left: `${previewLeft()}px`}">
<div style="background-color:#aaa; font-weight: bold" data-bind="text: previewConceptSet().name"></div>
<conceptset-quickview params="conceptSet: previewConceptSet"></conceptset-quickview>
</div>
<!-- /ko -->
</ul>

</div>

</div>
2 changes: 1 addition & 1 deletion js/components/conceptAddBox/concept-add-box.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ define([
super(params);
this.activeConceptSet = params.activeConceptSet || sharedState.activeConceptSet;
this.canCreateConceptSet = ko.pureComputed(function () {
return ((AuthAPI.isAuthenticated() && AuthAPI.isPermittedCreateConceptset()) || !config.userAuthenticationEnabled);
return AuthAPI.isPermittedCreateConceptset();
});
this.isActive = params.isActive || ko.observable(true);
this.onSubmit = params.onSubmit;
Expand Down
2 changes: 1 addition & 1 deletion js/components/conceptset/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ define([
this.loading = params.loading;
this.authApi = authApi;
this.canCreateConceptSet = ko.computed( () => {
return ((this.authApi.isAuthenticated() && this.authApi.isPermittedCreateConceptset()) || !config.userAuthenticationEnabled);
return this.authApi.isPermittedCreateConceptset();
});
this.newConceptSetName = ko.observable();
this.saveConceptSetShow = ko.observable();
Expand Down
2 changes: 1 addition & 1 deletion js/components/conceptsetmodal/conceptSetSaveModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ define(['knockout', 'appConfig', 'services/AuthAPI', 'services/ConceptSet', 'com
);
this.isNameUnique = ko.observable(false);

this.canCreate = ko.pureComputed(() => (authApi.isAuthenticated() && authApi.isPermittedCreateConceptset()) || !config.userAuthenticationEnabled);
this.canCreate = ko.pureComputed(() => authApi.isPermittedCreateConceptset());
this.canSave = ko.pureComputed(() => this.canCreate() && this.conceptSetName() && this.conceptSetName().length > 0 && this.isNameUnique());
}

Expand Down
56 changes: 28 additions & 28 deletions js/components/security/access/configure-access-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ define([
this.isModalShown = params.isModalShown;
this.isLoading = ko.observable(false);

this.writeRoleName = ko.observable();
this.writeAccessList = ko.observable([]);
this.writeRoleName = ko.observable();
this.writeAccessList = ko.observable([]);
this.writeRoleSuggestions = ko.observable([]);
this.writeRoleOptions = ko.computed(() => this.writeRoleSuggestions().map(r => r.name));
this.writeRoleSearch = ko.observable();
this.writeRoleSearch.subscribe(str => this.loadWriteRoleSuggestions(str));
this.writeRoleSearch.subscribe(str => this.loadWriteRoleSuggestions(str));

this.readAccessList = ko.observable([]);
this.readRoleName = ko.observable();
this.readRoleSuggestions = ko.observable([]);
this.readAccessList = ko.observable([]);
this.readRoleName = ko.observable();
this.readRoleSuggestions = ko.observable([]);
this.readRoleOptions = ko.computed(() => this.readRoleSuggestions().map(r => r.name));
this.readRoleSearch = ko.observable();
this.readRoleSearch.subscribe(str => this.loadReadRoleSuggestions(str));
Expand Down Expand Up @@ -58,7 +58,7 @@ define([
}
];

this.writeAccessColumns = [
this.writeAccessColumns = [
{
class: this.classes('access-tbl-col-id'),
title: ko.i18n('writeAccessColumns.id', 'ID'),
Expand All @@ -81,13 +81,13 @@ define([

async _loadReadAccessList() {
let accessList = await this.loadAccessListFn('READ');
accessList = accessList.map(a => ({ ...a, revoke: () => this.revokeRoleAccess(a.id, 'READ') }));
accessList = accessList.map(a => ({ ...a, revoke: () => this.revokeRoleAccess(a.id, 'READ') }));
this.readAccessList(accessList);
}

async _loadWriteAccessList() {
async _loadWriteAccessList() {
let accessList = await this.loadAccessListFn('WRITE');
accessList = accessList.map(a => ({ ...a, revoke: () => this.revokeRoleAccess(a.id, 'WRITE') }));
accessList = accessList.map(a => ({ ...a, revoke: () => this.revokeRoleAccess(a.id, 'WRITE') }));
this.writeAccessList(accessList);
}

Expand All @@ -96,16 +96,16 @@ define([
this.readRoleSuggestions(res);
}

async loadWriteRoleSuggestions() {
async loadWriteRoleSuggestions() {
const res = await this.loadRoleSuggestionsFn(this.writeRoleSearch());
this.writeRoleSuggestions(res);
}

async loadAccessList() {
this.isLoading(true);
try {
await this._loadReadAccessList();
await this._loadWriteAccessList();
await this._loadReadAccessList();
await this._loadWriteAccessList();
} catch (ex) {
console.log(ex);
}
Expand All @@ -115,28 +115,28 @@ define([
async grantAccess(perm_type) {
this.isLoading(true);
try {
if (perm_type == 'WRITE'){
const role = this.writeRoleSuggestions().find(r => r.name === this.writeRoleName());
await this.grantAccessFn(role.id,'WRITE');
await this._loadWriteAccessList();
this.writeRoleName('');
} else {
const role = this.readRoleSuggestions().find(r => r.name === this.readRoleName());
await this.grantAccessFn(role.id,'READ');
await this._loadReadAccessList();
this.readRoleName('');
}
if (perm_type == 'WRITE') {
const role = this.writeRoleSuggestions().find(r => r.name === this.writeRoleName());
await this.grantAccessFn(role.id, 'WRITE');
await this._loadWriteAccessList();
this.writeRoleName('');
} else {
const role = this.readRoleSuggestions().find(r => r.name === this.readRoleName());
await this.grantAccessFn(role.id, 'READ');
await this._loadReadAccessList();
this.readRoleName('');
}
} catch (ex) {
console.log(ex);
}
this.isLoading(false);
}

async revokeRoleAccess(roleId, perm_type) {
async revokeRoleAccess(roleId, perm_type) {
this.isLoading(true);
try {
await this.revokeAccessFn(roleId, perm_type);
await this.loadAccessList();
try {
await this.revokeAccessFn(roleId, perm_type);
await this.loadAccessList();
} catch (ex) {
console.log(ex);
}
Expand Down
2 changes: 0 additions & 2 deletions js/components/security/access/const.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ define([], function () {
FE_ANALYSIS: 'FE_ANALYSIS',
INCIDENCE_RATE: 'INCIDENCE_RATE',
SOURCE: 'SOURCE',
ESTIMATION: 'ESTIMATION',
PREDICTION: 'PREDICTION',
REUSABLE: 'REUSABLE'
};

Expand Down
4 changes: 2 additions & 2 deletions js/components/tags/modal/tags-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ define([
await this.assignTagFn(tag);
tag.assigned = true;
this.assignedTagsList.unshift(tag);
if (tag.groups.filter(tg => tg.id === this.currentTagGroup().id).length > 0) {
if (this.currentTagGroup() && tag.groups.filter(tg => tg.id === this.currentTagGroup().id).length > 0) {
this.tagsInGroupList.valueHasMutated();
}
} catch (ex) {
Expand Down Expand Up @@ -245,7 +245,7 @@ define([
const savedTag = savedTagRes.data;
await this.assignTag(savedTag);
this.allTagsList().unshift(savedTag);
if (this.newCustomTagGroup() === this.currentTagGroup().id) {
if (this.currentTagGroup() && this.newCustomTagGroup() === this.currentTagGroup().id) {
this.tagsInGroupList().unshift(savedTag);
this.sortByAssigned(this.tagsInGroupList);
this.tagsInGroupList.valueHasMutated();
Expand Down
6 changes: 3 additions & 3 deletions js/components/userbar/user-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ define([
componentParams: this.userJobParams,
});
this.selectedTabKey(constants.jobTypes.USER_JOB.title);
this.jobNotificationsPending = ko.computed(() => this.userJobParams.jobListing().filter(j => !j.viewed()).length);
this.jobNotificationsPending = ko.pureComputed(() => this.userJobParams.jobListing().filter(j => !j.viewed()).length);
} else {
this.selectedTabKey(constants.jobTypes.ALL_JOB.title);
this.jobNotificationsPending = ko.computed(() => this.allJobParams.jobListing().filter(j => !j.viewed()).length);
this.jobNotificationsPending = ko.pureComputed(() => this.allJobParams.jobListing().filter(j => !j.viewed()).length);
this.allJobParams.jobNameClick = this.jobNameClick.bind(this);
}
this.tabs.push({
Expand All @@ -73,7 +73,7 @@ define([
componentName: 'user-bar-jobs',
componentParams: this.allJobParams,
});
this.jobsCount = ko.computed(() => {
this.jobsCount = ko.pureComputed(() => {
if (this.selectedTabKey() === constants.jobTypes.USER_JOB.title) {
return this.userJobParams.jobListing().length;
}
Expand Down
Loading
Loading