Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addresses Issue #1465 : Added possibility to manage accounts in the UI. #1468

Merged
merged 9 commits into from
Jan 18, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ Donatello:bo
Rafael:sai
Michaelangelo:nunchaku}}}
4 users are defined here. Leonardo whose password is Katana, Donatello whose password is bo, Rafael whose password is sai, and Michaelangelo whose password is nunchaka.

!3 Hashed Passwords
A password hashing program, similar to unix's '''passwd''' command, is provided with FitNesse. The Password program has the following usage:
{{{Usage: java fitnesse.authentication.Password [-f <password file>] [-c <password cipher>] <user>
-f <password file> {passwords.txt}
-c <password cipher> {fitnesse.authentication.HashingCipher} }}}
!3 Hashed Passwords Created In Command Line
A password hashing program, similar to unix's '''passwd''' command, is provided with FitNesse.
By using the Password program with default setting and using the same usernames and passwords as above, the file ''passwords.txt'' will be generated with the following content:
{{{!fitnesse.authentication.HashingCipher
Leonardo:VEN4CfBvGCSafZDZNIKh
Expand All @@ -21,5 +17,15 @@ Michaelangelo:VBZ1TiB7HMptQsz3d3do
Rafael:YHVFNHr1fHaIGkLHMTSP}}}
You can see that the passwords have been hashed and are not humanly readable. You may also notice a new line.
!-!fitnesse.authentication.HashingCipher-!
This should be left as is in the file. It tells the program with PasswordCipher to use when hashing the passwords. You may create your own PasswordCipher by implementing the ''!-fitnesse.authentication.PasswordCipher-!'' interface and use it for creating password files using the -c command line argument.
This should be left as is in the file. It tells the program with PasswordCipher to use when hashing the passwords. You may create your own PasswordCipher by implementing the ''!-fitnesse.authentication.PasswordCipher-!'' interface and use it for creating password files.

The Password program can be used in the following two ways.
suratdas marked this conversation as resolved.
Show resolved Hide resolved
* '''Passwords Created In The FitNesse UI'''
* Ensure you have passwords.txt file located in the same location where fitnesse jar is present. Create admin user in that file.
suratdas marked this conversation as resolved.
Show resolved Hide resolved
* More passwords can be created in the FitNesse UI after logging in as admin user. Click on the link with admin user on any page to manage users. This uses the default PasswordCipher and default passwords.txt file.
suratdas marked this conversation as resolved.
Show resolved Hide resolved
* '''Hashed Passwords Created In Command Line'''
suratdas marked this conversation as resolved.
Show resolved Hide resolved
* The password files can be created using the -c command line argument.
suratdas marked this conversation as resolved.
Show resolved Hide resolved
{{{Usage: java fitnesse.authentication.Password [-f <password file>] [-c <password cipher>] <user>
-f <password file> {passwords.txt}
-c <password cipher> {fitnesse.authentication.HashingCipher} }}}

14 changes: 13 additions & 1 deletion src/fitnesse/authentication/Password.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public static void main(String[] args) throws Exception {
System.out.println("password saved in " + password.passwords.getName());
}

public boolean doesUserExist(String name) {
return passwords.getPasswordMap().get(name) != null;
}

public void deletePassword(String username) throws Exception {
passwords.deleteUser(username);
}

public static void printUsage() {
System.err.println("Usage: java fitnesse.authentication.Password [-f <password file>] [-c <password cipher>] <user>");
System.err.println("\t-f <password file> {" + defaultFile + "}");
Expand All @@ -44,7 +52,11 @@ public Password() throws Exception {
this(defaultFile);
}

public void savePassword() throws Exception {
public void savePassword(String usernamePassed, String passwordPassed) throws Exception {
passwords.savePassword(usernamePassed, passwordPassed);
}

private void savePassword() throws Exception {
passwords.savePassword(username, password);
}

Expand Down
7 changes: 7 additions & 0 deletions src/fitnesse/authentication/PasswordFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ public void savePassword(String user, String password) throws IOException {
savePasswords();
}

public void deleteUser(String user) throws Exception {
if (passwordMap.remove(user) == null) {
throw new Exception("User does not exist.");
}
savePasswords();
}

private void loadFile() throws IOException, ReflectiveOperationException {
LinkedList<String> lines = getPasswordFileLines();
loadCipher(lines);
Expand Down
3 changes: 3 additions & 0 deletions src/fitnesse/resources/bootstrap/templates/wikiNav.vm
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
<input type="hidden" name="searchScope" value="root" />
<input type="text" id="searchString" name="searchString" class="form-control" placeholder="Search for page" />
<input type="hidden" name="searchType" value="Search Titles" />
#if ($request.authorizationUsername)
User: <a href="$localPath?account">$request.authorizationUsername</a>
#end
</form>
47 changes: 47 additions & 0 deletions src/fitnesse/resources/templates/accountPage.vm
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<div class="well">
<h2>Change Password</h2>
#if($request.getAuthorizationUsername())
<form method="post">
<input type="hidden" name="responder" value="saveAccount"/>
<fieldset>
suratdas marked this conversation as resolved.
Show resolved Hide resolved
<label for="CurrentPasswordText">Current Password:</label>
<input type="text" id="CurrentPasswordText" name="CurrentPasswordText" size="70"/>
</fieldset>
<fieldset>
<label for="NewPasswordText">New Password:</label>
<input type="text" id="NewPasswordText" name="NewPasswordText" size="70"/>
</fieldset>
<fieldset>
<label for="ConfirmPasswordText">Confirm Password:</label>
<input type="text" id="ConfirmPasswordText" name="ConfirmPasswordText" size="70"/>
</fieldset>
<fieldset class="buttons">
<input type="submit" name="changePassword" value="Change My Password"/>
</fieldset>
</form>
#else
Log in to view details.
#end
</div>

#if($request.getAuthorizationUsername().equals("admin"))
<div class="well">
<h2>Create / Delete Users</h2>
<form method="post">
<input type="hidden" name="responder" value="saveAccount"/>
<p>For password reset, you can delete and re-create the user.</p>
<fieldset>
<label for="UserNameText">Username:</label>
<input type="text" id="UserNameText" name="UserNameText" size="70"/>
</fieldset>
<fieldset>
<label for="UserPasswordText">Password:</label>
<input type="text" id="UserPasswordText" name="UserPasswordText" placeholder="Not used for deleting users." size="70"/>
</fieldset>
<fieldset class="buttons">
<input type="submit" name="createUser" value="Create User"/>
<input type="submit" name="deleteUser" value="Delete User"/>
</fieldset>
</form>
</div>
#end
4 changes: 4 additions & 0 deletions src/fitnesse/responders/ResponderFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

import fitnesse.Responder;
import fitnesse.http.Request;
import fitnesse.responders.account.SaveAccountResponder;
import fitnesse.responders.editing.AddChildPageResponder;
import fitnesse.responders.editing.EditResponder;
import fitnesse.responders.editing.NewPageResponder;
import fitnesse.responders.editing.PropertiesResponder;
import fitnesse.responders.editing.SavePropertiesResponder;
import fitnesse.responders.editing.SaveResponder;
import fitnesse.responders.editing.SymbolicLinkResponder;
import fitnesse.responders.account.AccountResponder;
import fitnesse.responders.files.CreateDirectoryResponder;
import fitnesse.responders.files.DeleteConfirmationResponder;
import fitnesse.responders.files.DeleteFileResponder;
Expand Down Expand Up @@ -83,6 +85,8 @@ public ResponderFactory(String rootPath) {
addResponder("saveProperties", SavePropertiesResponder.class);
addResponder("searchProperties", SearchPropertiesResponder.class);
addResponder("variables", ScopeVariablesResponder.class);
addResponder("account", AccountResponder.class);
addResponder("saveAccount", SaveAccountResponder.class);
// Deprecated:
addResponder("executeSearchProperties", SearchPropertiesResponder.class);
addResponder("whereUsed", WhereUsedResponder.class);
Expand Down
97 changes: 97 additions & 0 deletions src/fitnesse/responders/account/AccountResponder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package fitnesse.responders.account;

import fitnesse.FitNesseContext;
import fitnesse.Responder;
import fitnesse.html.template.HtmlPage;
import fitnesse.html.template.PageTitle;
import fitnesse.http.Request;
import fitnesse.http.Response;
import fitnesse.http.SimpleResponse;
import fitnesse.responders.NotFoundResponder;
import fitnesse.wiki.MockingPageCrawler;
import fitnesse.wiki.PageData;
import fitnesse.wiki.PageCrawler;
import fitnesse.wiki.PathParser;
import fitnesse.wiki.WikiPage;
import fitnesse.wiki.WikiPagePath;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.UnsupportedEncodingException;

import static fitnesse.wiki.WikiPageProperty.*;

public class AccountResponder implements Responder {
private WikiPage page;
public PageData pageData;
private String resource;
private WikiPagePath path;
private SimpleResponse response;
private HtmlPage html;

@Override
public Response makeResponse(FitNesseContext context, Request request) throws Exception {

response = new SimpleResponse();
resource = request.getResource();
path = PathParser.parse(resource);
PageCrawler crawler = context.getRootPage().getPageCrawler();
page = crawler.getPage(path, new MockingPageCrawler());
if (page == null)
return new NotFoundResponder().makeResponse(context, request);

pageData = page.getData();
makeContent(context, request);
response.setMaxAge(0);
return response;
}

private void makeContent(FitNesseContext context, Request request) throws UnsupportedEncodingException {
if ("json".equals(request.getInput("format"))) {
JSONObject jsonObject = makeJson();
response.setContent(jsonObject.toString(1));
} else {
String html = makeHtml(context, request);
response.setContent(html);
}
}

private JSONObject makeJson() {
response.setContentType(Response.Format.JSON);
JSONObject jsonObject = new JSONObject();
if (pageData.hasAttribute(HELP)) {
jsonObject.put(HELP, pageData.getAttribute(HELP));
}
if (pageData.hasAttribute(SUITES)) {
JSONArray tags = new JSONArray();
for (String tag : pageData.getAttribute(SUITES).split(",")) {
if (StringUtils.isNotBlank(tag)) {
tags.put(tag.trim());
}
}
jsonObject.put(SUITES, tags);
}
return jsonObject;
}

private String makeHtml(FitNesseContext context, Request request) {
html = context.pageFactory.newPage();
html.setNavTemplate("viewNav");
html.put("viewLocation", request.getResource());
html.setTitle("Account: " + resource);

String tags = "";
if (pageData != null) {
tags = pageData.getAttribute(SUITES);
}

html.setPageTitle(new PageTitle("Account", path, tags));
html.put("pageData", pageData);
html.setMainTemplate("accountPage");

return html.html(request);

}

}
68 changes: 68 additions & 0 deletions src/fitnesse/responders/account/SaveAccountResponder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package fitnesse.responders.account;

import fitnesse.FitNesseContext;
import fitnesse.authentication.Password;
import fitnesse.http.Request;
import fitnesse.http.Response;
import fitnesse.http.SimpleResponse;
import fitnesse.responders.BasicResponder;
import fitnesse.responders.ErrorResponder;
import org.apache.commons.lang3.StringUtils;

public class SaveAccountResponder extends BasicResponder {
@Override
public Response makeResponse(FitNesseContext context, Request request) throws Exception {
if (request.getAuthorizationUsername() == null) {
return getResponse(context, "You have to be logged in to use this feature.");
}
Password password = new Password();
if (request.hasInput("changePassword")) {
String currentPassword = StringUtils.trim(request.getInput("CurrentPasswordText"));
String newPasswordText = StringUtils.trim(request.getInput("NewPasswordText"));
String confirmPasswordText = StringUtils.trim(request.getInput("ConfirmPasswordText"));
if (newPasswordText.isEmpty() || !newPasswordText.equals(confirmPasswordText)) {
return getResponse(context, "Password should not be empty and they should match.");
}
if (!currentPassword.equals(request.getAuthorizationPassword())) {
return getResponse(context, "Current password is incorrect.");
}
password.savePassword(request.getAuthorizationUsername(), newPasswordText);
} else if ("admin".equals(request.getAuthorizationUsername())) {
if (request.hasInput("createUser")) {
String newUserNameText = StringUtils.trim(request.getInput("UserNameText"));
String newUserPasswordText = StringUtils.trim(request.getInput("UserPasswordText"));
if (newUserNameText.isEmpty() || newUserPasswordText.isEmpty()) {
return getResponse(context, "Username or password field is empty.");
} else if (password.doesUserExist(newUserNameText)) {
return getResponse(context, "User already exists.");
}
password.savePassword(newUserNameText, newUserPasswordText);
} else if (request.hasInput("deleteUser")) {
String newUserNameText = StringUtils.trim(request.getInput("UserNameText"));
if ("admin".equals(newUserNameText)) {
return getResponse(context, "You cannot delete admin user.");
}
try {
password.deletePassword(newUserNameText);
} catch (Exception ex) {
return getResponse(context, ex.getMessage());
}
} else {
return getResponse(context, "Invalid input to modify account.");
}
} else {
return getResponse(context, "Only admin can create or delete users.");
}

Response response = new SimpleResponse();
response.redirect(context.contextRoot, request.getResource());
return response;
}

private static Response getResponse(FitNesseContext context, String message) throws Exception {
Response response = new ErrorResponder(message).makeResponse(context, null);
response.setStatus(412);
return response;
}

}
9 changes: 9 additions & 0 deletions test/fitnesse/authentication/PasswordFileTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ public void testSavePasswordForFirstUser() throws Exception {
assertSubString("Aladdin:" + cipher.encrypt("open sesame"), contents);
}

@Test
public void testDeleteUser() throws Exception {
passwords.savePassword("WillBeDeleted", "WillBeDeleted");
int beforeDeleting = passwords.getPasswordMap().size();
passwords.deleteUser("WillBeDeleted");
int afterDeleting = passwords.getPasswordMap().size();
assertEquals(1, beforeDeleting - afterDeleting);
}

@Test
public void testChangePasswordForFirstUser() throws Exception {
passwords.savePassword("Aladdin", "open sesame");
Expand Down
Loading