import { DataDiscoveryDatabase } from './datadiscovery.js'
import { SQLPassThruQuery } from './sqlpassthru.js'
import { BaseRequestHandler } from './baserequesthandler.js';
import { PKCE, generateCodeVerifier, generateCodeChallenge } from './pkce.js';
import { getUrlParms, isSSOredirect, popupCenter } from "./utils.js";
const SDB_SDBC_INVALID_HOSTNAME = 'Invalid hostname parameter, must be string';
const SDB_SDBC_INVALID_USERNAME = 'Invalid username parameter, must be string';
const SDB_SDBC_INVALID_APIKEY = 'Invalid apiKey parameter, must be string';
const SDB_SDBC_INVALID_PASSWORD = 'Invalid password parameter, must be string';
const SDB_SDBC_INVALID_IDPID = 'Invalid identity provider parameter, must be string';
const SDB_SDBC_INVALID_POPUP = 'Invalid popUp parameter, must be boolean';
const SDB_SDBC_INVALID_REDIRECT_URI = 'Invalid redirect uri parameter, must be string';
const SDB_SDBC_IDENTITY_PROVIDER_NOT_AVAILABLE = "Identity provider not available in settings";
/**
* Stores parameters necessary to communicate with a SlashDB instance and provides methods for retrieving metadata from the instance.
*/
class SlashDBClient {
/**
* Creates a SlashDB client to connect to a SlashDB instance.
*
* @param {Object} config
* @param {string} config.host - hostname/IP address of the SlashDB instance, including protocol and port number (e.g. http://192.168.1.1:8080)
* @param {string} config.apiKey - optional API key associated with username
* @param {Object} config.sso - optional settings to login with Single Sign-On
* @param {string} config.sso.idpId - optional identity provider id configured in SlashDB
* @param {string} config.sso.redirectUri - optional redirect uri to redirect browser after sign in
* @param {boolean} config.sso.popUp - optional flag to sign in against the identity provider with a Pop Up window (false by default)
*/
constructor(config) {
const host = config.host;
if (!host || typeof(host) !== 'string') {
throw TypeError(SDB_SDBC_INVALID_HOSTNAME);
}
this.host = host;
this.username = null;
this.apiKey = null;
this.basic = null;
this.sso = {
idpId: null,
redirectUri: null,
popUp: false
}
if (config.hasOwnProperty('apiKey')) {
const apiKey = config.apiKey;
if (!apiKey || typeof(apiKey) !== 'string') {
throw TypeError(SDB_SDBC_INVALID_APIKEY);
}
this.apiKey = apiKey;
} else if (config.hasOwnProperty('sso')) {
const idpId = config.sso.idpId;
const redirectUri = config.sso.redirectUri;
const popUp = config.sso.popUp;
if (!idpId || typeof(idpId) !== 'string') {
throw TypeError(SDB_SDBC_INVALID_IDPID);
}
if (!redirectUri || typeof(redirectUri) !== 'string') {
throw TypeError(SDB_SDBC_INVALID_REDIRECT_URI);
}
if (typeof(popUp) !== 'boolean') {
throw TypeError(SDB_SDBC_INVALID_POPUP);
}
this.sso.idpId = idpId;
this.sso.redirectUri = redirectUri;
this.sso.popUp = popUp;
}
this.ssoCredentials = null;
// create the special case BaseRequestHandler object for interacting with config endpoints
this.sdbConfig = new BaseRequestHandler(this);
// SlashDB config endpoints
this.loginEP = '/login';
this.logoutEP = '/logout';
this.settingsEP = '/settings.json';
this.versionEP = '/version.txt';
this.licenseEP = '/license';
this.loadEP = '/load-model';
this.unloadEP = '/unload-model';
this.checkDBConnEP = '/settings/check-database-connection.json';
this.reflectStatusEP = '/settings/reflect-status.json';
this.userEP = '/userdef';
this.dbDefEP = '/dbdef';
this.queryDefEP = '/querydef';
}
/**
* Logs in to SlashDB instance. Only required when using username/password based crendentials. if not provided will try SSO login.
*
* @param {string} username - optional username to use when connecting to SlashDB instance using password based login
* @param {string} password - optional password associated with username
* @returns {true} true - on successful login
* @throws {Error} on invalid login or error in login process
*/
async login(username, password) {
let body = {};
let sso = this.sso;
if (password) {
if (typeof(username) !== 'string') {
throw TypeError(SDB_SDBC_INVALID_USERNAME);
}
if (typeof(password) !== 'string') {
throw TypeError(SDB_SDBC_INVALID_PASSWORD);
}
body = { login: username, password: password };
let response = (await this.sdbConfig.post(body, this.loginEP)).res;
if (response.ok === true) {
this.basic = btoa(username + ":" + password);
this.username = username;
return true;
}
else {
return false;
}
} else if (sso.idpId && sso.redirectUri) {
await this.loginSSO(sso.popUp).then((resp) => {
this.ssoCredentials = resp;
});
let settings = (await this.sdbConfig.get(this.settingsEP)).data;
this.username = settings.user;
if (this.username === null || this.username === 'public'){
return false;
}
return true;
}
}
/**
* Updates a SlashDB client instance SSO settings.
*
* @param {Object} sso - optional settings to login with Single Sign-On
* @param {string} sso.idpId - optional identity provider id configured in SlashDB
* @param {string} sso.redirectUri - optional redirect uri to redirect browser after sign in
* @param {boolean} sso.popUp - optional flag to sign in against the identity provider with a Pop Up window (false by default)
*/
async updateSSO(sso) {
this.sso.idpId = sso.idpId ? sso.idpId : this.sso.idpId;
this.sso.redirectUri = sso.redirectUri ? sso.redirectUri : this.sso.redirectUri;
this.sso.popUp = sso.popUp ? sso.popUp : this.sso.popUp;
}
/**
* Builds a SSO session from a redirect url, if popUp is not used, this method must be used in the redirect page handler .
*/
async buildSSORedirect(){
const urlParams = getUrlParms();
if (isSSOredirect(urlParams)){
const ssoConfig = await this._getSsoConfig();
const url = window.location.href;
const pkce = new PKCE(ssoConfig);
this.sso.idpId = sessionStorage.getItem('ssoApp.idp_id');
pkce.codeVerifier = sessionStorage.getItem('ssoApp.code_verifier');
return new Promise((resolve, reject) => {
pkce.exchangeForAccessToken(url).then((resp) => {
this.ssoCredentials = resp;
resolve(true);
});
});
}
}
/**
* Logs in to SlashDB instance. Only required when using SSO.
* @param {boolean} popUp - optional flag to sign in against the identity provider with a Pop Up window (false by default)
*/
async loginSSO(popUp) {
popUp = popUp ? popUp : this.sso.popUp;
const ssoConfig = await this._getSsoConfig();
const pkce = new PKCE(ssoConfig);
const additionalParams = await this._buildSession();
let loginUrl = await pkce.authorizeUrl(additionalParams);
if (!popUp) {
window.location.replace(loginUrl);
}
const width = 500;
const height = 600;
const popupWindow = popupCenter(loginUrl, "login", width, height);
return new Promise((resolve, reject) => {
const checkPopup = setInterval(() => {
const pkce = new PKCE(ssoConfig);
let popUpHref = "";
try {
popUpHref = popupWindow.window.location.href;
} catch (e) {
console.warn(e);
}
if (popUpHref.startsWith(window.location.origin)) {
popupWindow.close();
pkce.codeVerifier = sessionStorage.getItem('ssoApp.code_verifier');
}
if (!popupWindow || !popupWindow.closed) return;
clearInterval(checkPopup);
pkce.exchangeForAccessToken(popUpHref).then((resp) => {
resolve(resp);
});
}, 250);
});
}
/**
* Refreshes the SSO access token.
*/
async refreshSSOToken(){
const ssoConfig = await this._getSsoConfig();
const pkce = new PKCE(ssoConfig);
const refreshToken = this.ssoCredentials.refresh_token;
return new Promise((resolve, reject) => {
pkce.refreshAccessToken(refreshToken).then((resp) => {
this.ssoCredentials = resp;
resolve(true);
});
});
}
/**
* Checks whether SlashDB client is authenticated against instance.
*
* @returns {boolean} boolean - to indicate if currently authenticated
*/
async isAuthenticated() {
const url = `${this.userEP}/${this.username}.json`;
try {
let response = (await this.sdbConfig.get(url)).res
if (response.ok === true) {
return true;
}
else {
return false;
}
}
catch(e) {
return false;
}
}
/**
* Logs out of SlashDB instance
*/
async logout() {
try {
await this.sdbConfig.get(this.logoutEP);
this.ssoCredentials = null;
this._clearSession();
}
catch(e) {
console.error(e);
}
}
/**
* Retrieves host's SlashDB configuration info
*
* @returns {object} containing SlashDB configuration items
*/
async getSettings() {
return (await this.sdbConfig.get(this.settingsEP)).data
}
/**
* Retrieves SlashDB version number
*
* @returns {string} containing SlashDB version number
*/
async getVersion() {
return (await this.sdbConfig.get(this.versionEP)).data;
}
/**
* Enables connection to a database configured on SlashDB host
*
* @param {string} [dbName] - SlashDB ID of database to connect
* @returns {object} containing database configuration info and connection status
*/
async loadModel(dbName) {
return (await this.sdbConfig.get(`${this.loadEP}/${dbName}`)).data;
}
/**
* Disables connection to a database configured on SlashDB host
*
* @param {string} [dbName] - SlashDB ID of database to disconnect
* @returns {object} containing database configuration info and connection status
*/
async unloadModel(dbName) {
return (await this.sdbConfig.get(`${this.unloadEP}/${dbName}`)).data;
}
/**
* Returns current status of SlashDB connection to database
*
* @param {string | undefined} [dbName] - SlashDB ID of database to filter on; leave empty to retrieve all databases
* @returns {object} containing database connection status for all or selected databases
*/
async getReflectStatus(dbName = undefined) {
const ep = (!dbName) ? this.reflectStatusEP : `${this.reflectStatusEP.split('.json')[0]}/${dbName}.json`;
return (await this.sdbConfig.get(ep)).data;
}
/**
* Returns configuration info about SlashDB users
*
* @param {string | undefined} [username] - SlashDB ID of user to filter on; leave empty to retrieve all users
* @returns {object} containing configuration info for all or selected users
*/
async getUser(username = undefined) {
const ep = (!username) ? this.userEP : `${this.userEP}/${username}`;
return (await this.sdbConfig.get(ep)).data;
}
/**
* Returns configuration info about SlashDB databases
*
* @param {string | undefined} [dbName] - database ID of database to filter on; leave empty to retrieve all databases
* @param {boolean} [guiData] - returns additional info normally available in the GUI view when set to true
* @returns {object} containing configuration info for all or selected databases
*/
async getDbDef(dbName = undefined, guiData = false) {
const guiParam = guiData ? '?guidata' : '';
const ep = (!dbName) ? `${this.dbDefEP}${guiParam}` : `${this.dbDefEP}/${dbName}${guiParam}`;
return (await this.sdbConfig.get(ep)).data;
}
/**
* Returns configuration info about SlashDB queries
*
* @param {string | undefined} [dbName] - SlashDB ID of query to filter on; leave empty to retrieve all databases
* @param {boolean} [guiData] - returns additional info normally available in the GUI view when set to true
* @returns {object} containing configuration info for all or selected queries
*/
async getQueryDef(queryName = undefined, guiData = false) {
const guiParam = guiData ? '?guidata' : '';
const ep = (!queryName) ? `${this.queryDefEP}${guiParam}` : `${this.queryDefEP}/${queryName}${guiParam}`;
return (await this.sdbConfig.get(ep)).data;
}
/**
* Retrieve a list of databases that are configured on the SlashDB instance
*
* @returns {Object} databases - a key/value pair object keyed by database ID, with
* a corresponding `DataDiscoveryDatabase` object for each key
*/
async getDatabases() {
const databases = {};
let dbList = await this.getReflectStatus();
for (const db in dbList) {
databases[db] = new DataDiscoveryDatabase(this, db);
}
return databases;
}
/**
* Retrieve a list of SQL Pass-Thru queries that are configured on the SlashDB instance
*
* @param {string} [dbName] - SlashDB database ID; if specified, will only return queries associated with the given database
* @returns {object} queries - a key/value pair object keyed by query ID, with
* a corresponding `SQLPassThruQuery` object for each key
*/
async getQueries(dbName = undefined) {
let queryList = await this.getQueryDef();
const queries = {};
// create a query object for each query in the list
for (const query in queryList) {
if (dbName) {
if (queryList[query]['database'] !== dbName) {
continue;
}
}
// required since url_template is not included when getQueryDef returns all queries, only for individual ones
const q = await this.getQueryDef(query);
// find parameters in URL template string and create list
let params = []
const tokens = q.url_template.match(/{(.*?)}/gm);
if (tokens) {
for (let t of tokens) {
t = t.replaceAll('{','').replaceAll('}','');;
params.push(t);
}
}
queries[q.query_id] = new SQLPassThruQuery(q.query_id, this.sdbClient, q.http_methods, params);
// remove HTTP methods that are disabled from the newly created query object
const methods = ['GET','POST','PUT','DELETE'];
for (const m of methods) {
if (q.http_methods.hasOwnProperty(m) && q.http_methods[m] === true) {
continue;
}
else {
queries[q.query_id][m.toLowerCase()] = null;
}
}
}
return queries;
}
async _getSsoConfig() {
let response = (await this.sdbConfig.get(this.settingsEP)).data;
let idpId = this.sso.idpId;
let redirectUri = this.sso.redirectUri;
const jwtSettings = response.auth_settings.authentication_policies.jwt
if (!jwtSettings.identity_providers.hasOwnProperty(this.sso.idpId)) {
throw new Error(SDB_SDBC_IDENTITY_PROVIDER_NOT_AVAILABLE);
}
const idpSettings = jwtSettings.identity_providers[this.sso.idpId]
const clientId = idpSettings.client_id;
const authorizationEndpoint = idpSettings.authorization_endpoint;
const tokenEndpoint = idpSettings.token_endpoint;
const requestedScopes = idpSettings.scope;
if (!redirectUri || typeof(redirectUri) !== 'string') {
redirectUri = idpSettings.redirect_uri;
}
const ssoConfig = {
idp_id: idpId,
client_id: clientId,
redirect_uri: redirectUri,
authorization_endpoint: authorizationEndpoint,
token_endpoint: tokenEndpoint,
requested_scopes: requestedScopes,
}
return ssoConfig;
}
async _buildSession() {
let state = generateCodeVerifier(128);
let nonce = generateCodeVerifier(128);
let codeChallengeMethod = 'S256';
let codeVerifier = generateCodeVerifier(128);
let codeChallenge = await generateCodeChallenge(codeVerifier);
let idpId = this.sso.idpId;
const additionalParams = {
code_challenge: codeChallenge,
code_challenge_method: codeChallengeMethod,
nonce: nonce,
response_mode: 'fragment',
response_type: 'code',
state: state
};
sessionStorage.setItem('ssoApp.idp_id', idpId);
sessionStorage.setItem('ssoApp.state', state);
sessionStorage.setItem('ssoApp.nonce', nonce);
sessionStorage.setItem('ssoApp.code_challenge_method', codeChallengeMethod);
sessionStorage.setItem('ssoApp.code_verifier', codeVerifier);
sessionStorage.setItem('ssoApp.code_challenge', codeChallenge);
return additionalParams;
}
_clearSession(){
sessionStorage.removeItem('ssoApp.idp_id');
sessionStorage.removeItem('ssoApp.state');
sessionStorage.removeItem('ssoApp.nonce');
sessionStorage.removeItem('ssoApp.code_challenge_method');
sessionStorage.removeItem('ssoApp.code_verifier');
sessionStorage.removeItem('ssoApp.code_challenge');
}
}
export { SlashDBClient }