Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1779,6 +1779,12 @@
"category": "IBM i",
"title": "Unload active profile",
"icon": "$(sign-out)"
},
{
"command": "code-for-ibmi.changePassword",
"enablement": "code-for-ibmi:connected",
"title": "Change password...",
"category": "IBM i"
}
],
"customEditors": [
Expand Down Expand Up @@ -2529,6 +2535,10 @@
{
"command": "code-for-ibmi.environment.profile.unload",
"when": "never"
},
{
"command": "code-for-ibmi.changePassword",
"when": "never"
}
],
"view/title": [
Expand Down
31 changes: 3 additions & 28 deletions src/api/IBMi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BindingValue } from "@ibm/mapepire-js";
import * as node_ssh from "node-ssh";
import path, { parse as parsePath } from 'path';
import { EventEmitter } from 'stream';
Expand Down Expand Up @@ -1359,7 +1360,7 @@ export default class IBMi {
* @param statements
* @returns a Result set
*/
async runSQL(statements: string | string[], options: { fakeBindings?: (string | number)[], forceSafe?: boolean } = {}): Promise<Tools.DB2Row[]> {
async runSQL(statements: string | string[], options: { bindings?: BindingValue[] } = {}): Promise<Tools.DB2Row[]> {
if (this.sqlJob) {
let list = Array.isArray(statements) ? statements : statements.split(`;`).filter(x => x.trim().length > 0);

Expand Down Expand Up @@ -1397,37 +1398,11 @@ export default class IBMi {
throw error;
}
} else {
if (isLast) {
// There is a bug with Mapepire handling of binding parameters.
// We work around it by using these fake parameters and passing
// in UTF8 encoding strings/numbers.
const fakeBindings = options.fakeBindings;
if (statement.includes(`?`) && fakeBindings && fakeBindings.length > 0) {
const parts = statement.split(`?`);

statement = ``;
for (let partsIndex = 0; partsIndex < parts.length; partsIndex++) {
statement += parts[partsIndex];
if (fakeBindings[partsIndex] !== undefined) {
switch (typeof fakeBindings[partsIndex]) {
case `number`:
statement += fakeBindings[partsIndex];
break;

case `string`:
statement += Tools.bufferToUx(fakeBindings[partsIndex] as string);
break;
}
}
}
}
}

let query;
let error: Tools.SqlError | undefined;
const log = `Running SQL query: ${statement}\n`;
try {
query = this.sqlJob.query<Tools.DB2Row>(statement);
query = this.sqlJob.query<Tools.DB2Row>(statement, { parameters: options.bindings });
const rs = await query.execute(99999);
if (rs.has_results) {
lastResultSet.push(...rs.data);
Expand Down
7 changes: 0 additions & 7 deletions src/api/Tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ export namespace Tools {

export interface DB2Row extends Record<string, string | number | null> { }

export function bufferToUx(input: string) {
const hexString = Array.from(input)
.map(char => char.charCodeAt(0).toString(16).padStart(4, '0').toUpperCase())
.join('');
return `UX'${hexString}'`;
}

export function makeid(length: number = 8) {
let text = `O_`;
const possible =
Expand Down
102 changes: 102 additions & 0 deletions src/api/components/password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { posix } from "path";
import IBMi from "../IBMi";
import { Tools } from "../Tools";
import { ComponentIdentification, ComponentState, IBMiComponent } from "./component";

export class PasswordManager implements IBMiComponent {
static readonly ID = "CHGPWD";
static readonly VERSION = 1;

getIdentification(): ComponentIdentification {
return { name: PasswordManager.ID, version: 1 };
}

async setInstallDirectory?(_installDirectory: string) {
//Not used
}

async getRemoteState(connection: IBMi, _installDirectory: string) {
let version = 0;
const [result] = await connection.runSQL(`select cast(LONG_COMMENT as VarChar(200)) LONG_COMMENT from qsys2.sysprocs where routine_schema = '${connection.getConfig().tempLibrary.toUpperCase()}' and routine_name = '${PasswordManager.ID}'`);
if (result?.LONG_COMMENT) {
const comment = result.LONG_COMMENT as string;
const dash = comment.indexOf('-');
if (dash > -1) {
version = Number(comment.substring(0, dash).trim());
}
}
if (version < PasswordManager.VERSION) {
return `NeedsUpdate`;
}

return `Installed`;
}

async update(connection: IBMi, _installDirectory: string): Promise<ComponentState> {
try {
await connection.withTempDirectory(async directory => {
const source = posix.join(directory, `${PasswordManager.ID}.sql`);
const procedure = `${connection.getConfig().tempLibrary}.${PasswordManager.ID}`;
await connection.getContent().writeStreamfileRaw(source, /* sql */`
create or replace procedure ${procedure}(oldPassword varchar(128), newPassword varchar(128))
language sql
not deterministic
begin
call QSYS.QSYCHGPW(
'*CURRENT ', oldPassword, newPassword,
X'00000000',
LENGTH(oldPassword), 0, LENGTH(newPassword), 0
);
end;

comment on procedure ${procedure} is '${PasswordManager.VERSION} - Change password';
call QSYS2.QCMDEXC('grtobjaut ${connection.getConfig().tempLibrary}/${PasswordManager.ID} *PGM *PUBLIC *ALL');
`);
const compile = await connection.runCommand({
command: `RUNSQLSTM SRCSTMF('${source}') COMMIT(*NONE) NAMING(*SQL) OPTION(*NOSRC)`,
noLibList: true
});
if (compile.code !== 0) {
throw Error(compile.stderr || compile.stdout);
}
});
return "Installed";
}
catch (error: any) {
connection.appendOutput(`Failed to install ${PasswordManager.ID} procedure:\n${typeof error === "string" ? error : JSON.stringify(error)}`);
return "Error";
}
}

async getPasswordExpiration(connection: IBMi) {
const [row] = (await connection.runSQL(`
Select EXTRACT(EPOCH FROM (DATE_PASSWORD_EXPIRES)) * 1000 AS EXPIRATION,
DAYS(DATE_PASSWORD_EXPIRES) - DAYS(current_timestamp) as DAYS_LEFT
FROM TABLE (QSYS2.QSYUSRINFO('${connection.upperCaseName(connection.currentUser)}'))
`));
if (row && row.EXPIRATION) {
return {
expiration: new Date(Number(row.EXPIRATION)),
daysLeft: Number(row.DAYS_LEFT)
}
}
}

async changePassword(connection: IBMi, oldPassword: string, newPassword: string) {
try {
await connection.runSQL(`call ${connection.getConfig().tempLibrary}.${PasswordManager.ID}(?, ?)`, { bindings: [oldPassword, newPassword] });
}
catch (error: any) {
if (error instanceof Tools.SqlError) {
const message = /(\[.*\] )?(.*), \d+/.exec(error.message)?.[2]; //try to keep only the relevent part of the error
throw new Error(message || error.message);
}
else if (error instanceof Error) {
throw error
}

throw Error(String(error));
}

}
}
9 changes: 9 additions & 0 deletions src/api/configuration/storage/ConnectionStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const SOURCE_LIST_KEY = `sourceList`;
const DEPLOYMENT_KEY = `deployment`;
const DEBUG_KEY = `debug`;
const MESSAGE_SHOWN_KEY = `messageShown`;
const LAST_PASSWORD_CHECK = `lastPasswordCheck`;

const RECENTLY_OPENED_FILES_KEY = `recentlyOpenedFiles`;
const AUTHORISED_EXTENSIONS_KEY = `authorisedExtensions`
Expand Down Expand Up @@ -148,4 +149,12 @@ export class ConnectionStorage {
await this.internalStorage.set(MESSAGE_SHOWN_KEY, shownMessages.filter(message => message !== messageId));
}
}

getNextPasswordCheck() {
return new Date(this.internalStorage.get<number>(LAST_PASSWORD_CHECK) || 0);
}

setNextPasswordCheck(epoch: number) {
return this.internalStorage.set(LAST_PASSWORD_CHECK, epoch);
}
}
31 changes: 10 additions & 21 deletions src/api/tests/suites/encoding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,32 +27,21 @@ describe('Encoding tests', { concurrent: true }, () => {
});

it('Prove that input strings are NOT messed up by CCSID', async () => {
let howManyTimesItMessedUpTheResult = 0;

for (const strCcsid in contents) {
const data = contents[strCcsid as keyof typeof contents].join(``);

const sqlA = `select ? as THEDATA from sysibm.sysdummy1`;
const resultA = await connection?.runSQL(sqlA, { fakeBindings: [data], forceSafe: true });
expect(resultA?.length).toBeTruthy();

const sqlB = `select '${data}' as THEDATA from sysibm.sysdummy1`;
const resultB = await connection?.runSQL(sqlB, { forceSafe: true });
expect(resultB?.length).toBeTruthy();

expect(resultA![0].THEDATA).toBe(data);
if (resultB![0].THEDATA !== data) {
howManyTimesItMessedUpTheResult++;
}
const sql = `select '${data}' as THEDATA from sysibm.sysdummy1`;
const result = await connection?.runSQL(sql);

expect(result.length).toBeTruthy();
expect(result[0].THEDATA).toBe(data);
}

expect(howManyTimesItMessedUpTheResult).toBe(0);
});

it('Compare Unicode to EBCDIC successfully', async () => {

const sql = `select table_name, table_owner from qsys2.systables where table_schema = ? and table_name = ?`;
const result = await connection?.runSQL(sql, { fakeBindings: [`QSYS2`, `SYSCOLUMNS`] });
const result = await connection?.runSQL(sql, { bindings: [`QSYS2`, `SYSCOLUMNS`] });
expect(result?.length).toBeTruthy();
});

Expand Down Expand Up @@ -180,15 +169,15 @@ describe('Encoding tests', { concurrent: true }, () => {
skipLibrary = true;
}

let result = await connection.runCommand({command: `CRTSRCPF FILE(${library}/${sourceFile}) RCDLEN(112)`});
let result = await connection.runCommand({ command: `CRTSRCPF FILE(${library}/${sourceFile}) RCDLEN(112)` });
expect(result.code).toBe(0);

for (const member of members) {
result = await connection.runCommand({command: `ADDPFM FILE(${library}/${sourceFile}) MBR(${member}) SRCTYPE(TXT) TEXT('Test ${member}')`});
result = await connection.runCommand({ command: `ADDPFM FILE(${library}/${sourceFile}) MBR(${member}) SRCTYPE(TXT) TEXT('Test ${member}')` });
expect(result.code).toBe(0);
}

result = await connection.runCommand({command: `CRTDTAARA DTAARA(${library}/${dataArea}) TYPE(*CHAR) LEN(50) VALUE('hi')`});
result = await connection.runCommand({ command: `CRTDTAARA DTAARA(${library}/${dataArea}) TYPE(*CHAR) LEN(50) VALUE('hi')` });
expect(result.code).toBe(0);

if (!skipLibrary) {
Expand Down Expand Up @@ -292,7 +281,7 @@ describe('Encoding tests', { concurrent: true }, () => {

await connection.runCommand({ command: `DLTF FILE(${tempLib}/${testFile})`, noLibList: true });

const createResult = await connection.runCommand({command: `CRTSRCPF FILE(${tempLib}/${testFile}) RCDLEN(112)`});
const createResult = await connection.runCommand({ command: `CRTSRCPF FILE(${tempLib}/${testFile}) RCDLEN(112)` });
expect(createResult.code).toBe(0);
try {
const addPf = await connection.runCommand({ command: `ADDPFM FILE(${tempLib}/${testFile}) MBR(${testMember}) SRCTYPE(TXT)`, noLibList: true });
Expand Down
92 changes: 90 additions & 2 deletions src/commands/password.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
import { commands, extensions, window, Disposable, ExtensionContext } from "vscode";
import { commands, Disposable, ExtensionContext, extensions, l10n, ProgressLocation, window } from "vscode";
import Instance from "../Instance";
import { getStoredPassword } from "../config/passwords";
import { PasswordManager } from "../api/components/password";
import { getStoredPassword, setStoredPassword } from "../config/passwords";
import { CustomUI } from "../webviews/CustomUI";


const passwordAttempts: { [extensionId: string]: number } = {}

export function registerPasswordCommands(context: ExtensionContext, instance: Instance): Disposable[] {
instance.subscribe(context, "connected", "Check password expiration", async () => {
const nextCheck = instance.getStorage()?.getNextPasswordCheck();
//Never checked or next check is overdue
const today = new Date();
if (!nextCheck || today > nextCheck) {
const connection = instance.getConnection();
const expiration = await connection?.getComponent<PasswordManager>(PasswordManager.ID)?.getPasswordExpiration(connection);
let whenNextDay = 7; //Next check in one week by default
if (expiration) {
if (expiration.daysLeft < 7) {
//Less than a week left: check every day
whenNextDay = 1;
}
else if (expiration.daysLeft < 14) {
//Less than two weeks left: check again when there will be one week left
whenNextDay = expiration.daysLeft - 7;
}

if (expiration.daysLeft <= 14) { //Warn at least two weeks before expiration
window.showInformationMessage(l10n.t("Your IBM i password will expire in {0} day(s); do you want to change it now?", expiration.daysLeft), { modal: true }, l10n.t("Change password"))
.then(change => {
if (change) {
commands.executeCommand("code-for-ibmi.changePassword");
}
});
}
}

instance.getStorage()?.setNextPasswordCheck(today.setDate(today.getDate() + whenNextDay));
}
});

return [
commands.registerCommand(`code-for-ibmi.getPassword`, async (extensionId: string, reason?: string) => {
if (extensionId) {
Expand Down Expand Up @@ -82,6 +116,60 @@ export function registerPasswordCommands(context: ExtensionContext, instance: In
}
}
}
}),
commands.registerCommand("code-for-ibmi.changePassword", async () => {
const connection = instance.getConnection();
if (connection) {
let currentPassword = "";
let newPassword = "";
let done = false;
let error = "";
while (!done) {
const form = new CustomUI().addHeading(l10n.t("Change password for {0} on {1}", connection.currentUser, connection.currentConnectionName));
if (error) {
form.addParagraph(`<span style="color: var(--vscode-errorForeground)">${error}</span>`);
}
const page = (await form.addPassword("currentPassword", l10n.t("Current password"), '', currentPassword)
.addPassword("newPassword", l10n.t("New password"), '', newPassword)
.addPassword("newPasswordConfirm", l10n.t("Confirm new password"), '')
.addButtons({ id: "apply", label: l10n.t("Change password"), requiresValidation: false })
.loadPage<{ currentPassword: string, newPassword: string, newPasswordConfirm: string }>(l10n.t("Password change")));

if (page?.data) {
const data = page.data;
currentPassword = data.currentPassword;
newPassword = data.newPassword;
if (!currentPassword || !newPassword || !data.newPasswordConfirm) {
error = l10n.t("Every password field must be filled.")
}
else if (data.currentPassword === data.newPassword) {
error = l10n.t("New password must be different from the current one.")
}
else if (data.newPassword !== data.newPasswordConfirm) {
error = l10n.t("New password field and confirmation field don't match.")
}
else {
try {
await window.withProgress({ title: l10n.t("Changing password..."), location: ProgressLocation.Notification }, async () => await connection.getComponent<PasswordManager>(PasswordManager.ID)?.changePassword(connection, currentPassword, newPassword));
await setStoredPassword(context, connection.currentConnectionName, newPassword);
const today = new Date();
await instance.getStorage()?.setNextPasswordCheck(today.setDate(today.getDate() + 7));
window.showInformationMessage(l10n.t("Password successfully changed for {0} on {1}", connection.currentUser, connection.currentConnectionName));
done = true;
}
catch (e: any) {
error = l10n.t("Password change failed: {0}", e instanceof Error ? e.message : String(e));
}
}
}
else {
done = true;
}
page?.panel.dispose();
}
} else {
throw new Error(`Not connected to an IBM i.`);
}
})
]
}
Loading
Loading