Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,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 @@ -2507,6 +2513,10 @@
{
"command": "code-for-ibmi.environment.profile.unload",
"when": "never"
},
{
"command": "code-for-ibmi.changePassword",
"when": "never"
}
],
"view/title": [
Expand Down Expand Up @@ -3247,4 +3257,4 @@
"halcyontechltd.vscode-ibmi-walkthroughs",
"vscode.git"
]
}
}
72 changes: 72 additions & 0 deletions src/api/components/password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { posix } from "path";
import IBMi from "../IBMi";
import { ComponentIdentification, ComponentState, IBMiComponent } from "./component";

export class PasswordManager implements IBMiComponent {
static ID = "IBM i Password Manager";

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

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

getRemoteState(_connection: IBMi, _installDirectory: string): ComponentState {
//Virtual component, always installed
return "Installed";
}

update(_connection: IBMi, _installDirectory: string): ComponentState {
//Virtual component, always installed
return "Installed";
}

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, additionalAuthenticationFactor?: string) {
return await connection.withTempDirectory(async directory => {
const source = posix.join(directory, "ChangePassword.java");
const as400Class = connection.getConfig().secureSQL ? "SecureAS400" : "AS400"; //If Mapepire can/must use TLS, then this too.
await connection.getContent().writeStreamfileRaw(source, `
import com.ibm.as400.access.${as400Class};
public class ChangePassword {
public static void main(String[] args) throws Exception {
try (final ${as400Class} ibmi = new ${as400Class}()){
ibmi.changePassword("${oldPassword}".toCharArray(), "${newPassword}".toCharArray()${additionalAuthenticationFactor ? `, "${additionalAuthenticationFactor}".toCharArray()` : ""});
}
catch(Exception e){
System.err.println(e.getMessage());
System.exit(1);
}
}
}
`);
const change = await connection.sendCommand({
command: [
`javac -cp /QIBM/ProdData/OS400/jt400/lib/jt400.jar ChangePassword.java`,
`rm -f ChangePassword.java`,
`java -cp /QIBM/ProdData/OS400/jt400/lib/jt400.jar:. ChangePassword`
].join(" && "),
directory
});
if (change.code !== 0) {
//Cleanup: AS400SecurityException usually ends with ":<user>" - we remove it
throw Error((change.stderr || change.stdout).replaceAll(`:${connection.currentUser}`, ""));
}
});
}
}
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);
}
}
89 changes: 87 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,57 @@ 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.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.`);
}
})
]
}
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CustomQSh } from "./api/components/cqsh";
import { GetMemberInfo } from "./api/components/getMemberInfo";
import { GetNewLibl } from "./api/components/getNewLibl";
import { extensionComponentRegistry } from "./api/components/manager";
import { PasswordManager } from "./api/components/password";
import { parseErrors } from "./api/errors/parser";
import { CustomCLI } from "./api/tests/components/customCli";
import { onCodeForIBMiConfigurationChange } from "./config/Configuration";
Expand Down Expand Up @@ -126,6 +127,7 @@ export async function activate(context: ExtensionContext): Promise<CodeForIBMi>
extensionComponentRegistry.registerComponent(context, new GetNewLibl);
extensionComponentRegistry.registerComponent(context, new GetMemberInfo());
extensionComponentRegistry.registerComponent(context, new CopyToImport());
extensionComponentRegistry.registerComponent(context, new PasswordManager());

registerURIHandler(context,
sandboxURIHandler,
Expand Down
1 change: 1 addition & 0 deletions src/instantiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ async function updateConnectedBar() {
`[$(settings-gear) Settings](command:code-for-ibmi.showAdditionalSettings)`,
terminalMenuItem,
actionsMenuItem,
`[$(key) Change password](command:code-for-ibmi.changePassword)`,
debugPTFInstalled(connection) ?
`[$(${debugRunning ? "bug" : "debug"}) Debugger ${((await getDebugServiceDetails(connection)).version)} (${debugRunning ? "on" : "off"})](command:ibmiDebugBrowser.focus)`
:
Expand Down
Loading