Skip to content

Commit

Permalink
Addresses Issue #1465 : Added possibility to manage accounts in the U…
Browse files Browse the repository at this point in the history
…I. (#1468)
  • Loading branch information
suratdas authored Jan 18, 2024
1 parent bd43869 commit 91b8632
Show file tree
Hide file tree
Showing 14 changed files with 797 additions and 19 deletions.
3 changes: 2 additions & 1 deletion FitNesseRoot/FitNesse/ReleaseNotes/content.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
!2 Pending Changes
* Added the defined values to the overview of all variables in scope. ([[1472][https://github.com/unclebob/fitnesse/pull/1472]])
* Ability to change password and additionally create/delete users feature for admin([[1468][https://github.com/unclebob/fitnesse/pull/1468])

!2 20231203
* Updated README and dependencies
* Updated README and dependencies

!2 20231029
* Drop Java 8 compatibility and compile with Java 11, Use OpenJDK's nashorn dependency to evaluate JS expressions ([[1426][https://github.com/unclebob/fitnesse/pull/1426]])
Expand Down
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 file can be managed in the following two ways:
* '''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, using the command line tool described below (passing the -c option creates the file).
* When you are logged in (e.g. by accessing a secure page), you will see a User link in the top right corner of any page. Clicking that will take you to the page where you can change your password. The "admin" user can also create or delete other users. This uses the default PasswordCipher and default passwords.txt file.
* '''Updating the password file via the command line'''
* Call the Password program supplied with !-FitNesse-! to add a user with a password (passing the '-c' command line argument creates the file)
{{{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>
<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

0 comments on commit 91b8632

Please sign in to comment.