Merge branch 'master' into translation

This commit is contained in:
Axel Uhl committed 2020-01-17 13:07:22 +01:00
commit e7b7a95dda
34 files changed
+859 -85

No files matched your search

@@ -1,14 +1,17 @@
package com.sap.sailing.domain.common;
public class BranchIOConstants {
public static final String SAILINSIGHT_2_APP_BRANCHIO = "https://sailinsight20-app.sapsailing.com/invite";
public static final String SAILINSIGHT_APP_BRANCHIO = "https://sailinsight-app.sapsailing.com/invite";
public static final String SAILINSIGHT_2_APP_BRANCHIO = "https://sailinsight20-app.sapsailing.com/invite";
public static final String SAILINSIGHT_3_APP_BRANCHIO = "https://sailinsight30-app.sapsailing.com/invite";
public static final String SAILINSIGHT_APP_BRANCHIO_PATH = "checkinUrl";
public static final String BUOYPINGER_APP_BRANCHIO = "https://buoypinger-app.sapsailing.com/invite";
public static final String BUOYPINGER_APP_BRANCHIO_PATH = "checkinUrl";
public static final String OPEN_REGATTA_2_APP_BRANCHIO = "https://sailinsight20-app.sapsailing.com/publicInvite";
public static final String OPEN_REGATTA_2_APP_BRANCHIO_PATH = "checkinUrl";
public static final String OPEN_REGATTA_3_APP_BRANCHIO = "https://sailinsight30-app.sapsailing.com/publicInvite";
public static final String OPEN_REGATTA_3_APP_BRANCHIO_PATH = "checkinUrl";
private BranchIOConstants() {
};
}
}
@@ -3,7 +3,8 @@ package com.sap.sailing.domain.common;
public enum MailInvitationType {
LEGACY,
SailInsight1,
SailInsight2;
SailInsight2,
SailInsight3;
public static final String SYSTEM_PROPERTY_NAME = "com.sap.sailing.domain.tracking.MailInvitationType";
}
@@ -1159,6 +1159,11 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
UUID id = (UUID) eventDBObject.get(FieldNames.EVENT_ID.name());
TimePoint startDate = loadTimePoint(eventDBObject, FieldNames.EVENT_START_DATE);
TimePoint endDate = loadTimePoint(eventDBObject, FieldNames.EVENT_END_DATE);
if (endDate.before(startDate)) {
logger.warning("End date "+endDate+" of event "+name+" with ID "+id+" is before its start date "+
startDate+"; adjusting such that end date equals start date.");
endDate = startDate;
}
boolean isPublic = eventDBObject.get(FieldNames.EVENT_IS_PUBLIC.name()) != null
? (Boolean) eventDBObject.get(FieldNames.EVENT_IS_PUBLIC.name()) : false;
Venue venue = loadVenue((Document) eventDBObject.get(FieldNames.VENUE.name()));
@@ -5,7 +5,6 @@ import java.util.Locale;
import com.sap.sailing.domain.common.BranchIOConstants;
public class BranchIO2RaceLogTrackingInvitationMailBuilder extends BranchIORaceLogTrackingInvitationMailBuilder {
BranchIO2RaceLogTrackingInvitationMailBuilder(Locale locale) {
super(locale);
}
@@ -20,5 +19,4 @@ public class BranchIO2RaceLogTrackingInvitationMailBuilder extends BranchIORaceL
// FIXME the app should in the future at some point also support bouy pinging, replace this then
return BranchIOConstants.BUOYPINGER_APP_BRANCHIO;
}
}
@@ -0,0 +1,24 @@
package com.sap.sailing.domain.racelogtracking.impl;
import java.util.Locale;
import com.sap.sailing.domain.common.BranchIOConstants;
public class BranchIO3RaceLogTrackingInvitationMailBuilder extends BranchIORaceLogTrackingInvitationMailBuilder {
BranchIO3RaceLogTrackingInvitationMailBuilder(Locale locale) {
super(locale);
}
@Override
protected String getSailInsightBranchIO() {
return BranchIOConstants.SAILINSIGHT_3_APP_BRANCHIO;
}
@Override
protected String getBouyPingerBranchIO() {
// FIXME the app should in the future at some point also support bouy pinging, replace this then
return BranchIOConstants.BUOYPINGER_APP_BRANCHIO;
}
}
@@ -362,6 +362,9 @@ public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter {
case SailInsight2:
mail = new BranchIO2RaceLogTrackingInvitationMailBuilder(locale);
break;
case SailInsight3:
mail = new BranchIO3RaceLogTrackingInvitationMailBuilder(locale);
break;
default:
throw new IllegalArgumentException("Unhandled mail type");
}
@@ -7,6 +7,7 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
@@ -330,7 +331,7 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
builder = child.getTextContent();
break;
case "dxtDate":
issueDate = new MillisecondsTimePoint(isoTimestampFormat.parse(child.getTextContent()+"+0000")); // assume UTC
issueDate = new MillisecondsTimePoint(parseDate(child.getTextContent())); // assume UTC
break;
case "CertType":
certType = Integer.valueOf(child.getTextContent().trim());
@@ -350,6 +351,19 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
sailNumber, boatClassName, designer, builder, yearBuilt, issueDate, certType, isOneDesign, isProvisional);
}
private Date parseDate(String dateString) throws ParseException {
StringBuilder stringBuilder = new StringBuilder(dateString);
char timeZoneCharacter = stringBuilder.charAt(stringBuilder.length()-1);
if(timeZoneCharacter== 'z' || timeZoneCharacter == 'Z'|| timeZoneCharacter == 'X') {
stringBuilder.deleteCharAt(stringBuilder.length()-1);
}
if(false == stringBuilder.toString().contains("+")) {
stringBuilder.append("+0000");
}
return isoTimestampFormat.parse(stringBuilder.toString());
}
@Override
public ORCCertificate getCertificate(String referenceNumber) throws Exception {
final HttpClient client = new SystemDefaultHttpClient();
@@ -166,6 +166,7 @@ public class QRCodePlace extends AbstractBasePlace {
}
public String getPublicInviteBranchIOUrl() {
// TODO why is this specific to version 2 and not considering the default server setting for mailInvitationType?
return BranchIOConstants.OPEN_REGATTA_2_APP_BRANCHIO + "?" + QRCodePlace.PARAM_REGATTA_NAME + "="
+ encodeUrl(publicRegattaName) + "&" + QRCodePlace.PARAM_REGATTA_SECRET + "="
+ encodeUrl(regattaRegistrationLinkSecret) + "&" + QRCodePlace.PARAM_SERVER + "="
@@ -144,12 +144,9 @@ public class RegattaDetailsComposite extends Composite {
Button button = new Button(stringMessages.registrationLinkShare(), new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
RegistrationLinkWithQRCode registrationLinkWithQRCode = new RegistrationLinkWithQRCode();
registrationLinkWithQRCode.setSecret(regatta.registrationLinkSecret);
sailingService.getMailType(new AsyncCallback<MailInvitationType>() {
@Override
public void onFailure(Throwable caught) {
logger.log(Level.SEVERE, "Could not determine MailInvitationType", caught);
@@ -221,21 +221,28 @@ public class RegattaLogAddDeviceMappingDialog extends DataEntryDialogWithDateTim
String url = DeviceMappingConstants.getDeviceMappingForRegattaLogUrl(baseUrlWithoutTrailingSlash,
eventIdAsString, leaderboardName, mappedItemType, mappedItemId, regattaRegisterSecret,
GwtUrlHelper.INSTANCE);
if (mailInvitationType == MailInvitationType.LEGACY) {
switch (mailInvitationType) {
case LEGACY:
// URL is already legacy
} else if (mailInvitationType == MailInvitationType.SailInsight1) {
break;
case SailInsight1:
url = BranchIOConstants.SAILINSIGHT_APP_BRANCHIO + "?"
+ BranchIOConstants.SAILINSIGHT_APP_BRANCHIO_PATH + "="
+ GwtUrlHelper.INSTANCE.encodeQueryString(url);
} else if (mailInvitationType == MailInvitationType.SailInsight2) {
break;
case SailInsight2:
url = BranchIOConstants.SAILINSIGHT_2_APP_BRANCHIO + "?"
+ BranchIOConstants.OPEN_REGATTA_2_APP_BRANCHIO_PATH + "="
+ GwtUrlHelper.INSTANCE.encodeQueryString(url);
}
else {
break;
case SailInsight3:
url = BranchIOConstants.SAILINSIGHT_3_APP_BRANCHIO + "?"
+ BranchIOConstants.OPEN_REGATTA_3_APP_BRANCHIO_PATH + "="
+ GwtUrlHelper.INSTANCE.encodeQueryString(url);
break;
default:
throw new QRCodeURLCreationException("Unknown mail invitation type: " + mailInvitationType);
}
return url;
}
});
@@ -63,14 +63,10 @@ public class RegistrationLinkWithQRCodeDialog extends DataEntryDialog<Registrati
this.invitationType = invitationType;
this.registrationLinkWithQRCode = registrationLinkWithQRCode == null ? new RegistrationLinkWithQRCode()
: registrationLinkWithQRCode;
registrationLinkPanel = new CaptionPanel(stringMessages.registrationLinkUrl());
registrationLinkPanelContent = new VerticalPanel();
registrationLinkPanel.add(registrationLinkPanelContent);
final Label eventExplain = new Label(stringMessages.event());
events = createGenericListBox(new ValueBuilder<EventDTO>() {
@Override
public String getValue(EventDTO item) {
@@ -98,10 +94,8 @@ public class RegistrationLinkWithQRCodeDialog extends DataEntryDialog<Registrati
updateDisplayWidgets();
}
});
registrationLinkPanelContent.add(eventExplain);
registrationLinkPanelContent.add(events);
registrationLinkExplain = new Label(stringMessages.registrationLinkUrlExplain());
registrationLinkPanelContent.add(registrationLinkExplain);
urlTextBox = createTextBox("URL", 100);
@@ -109,7 +103,6 @@ public class RegistrationLinkWithQRCodeDialog extends DataEntryDialog<Registrati
registrationLinkPanelContent.add(urlTextBox);
Anchor copyAnchor = new Anchor(stringMessages.registrationLinkUrlCopy());
copyAnchor.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
urlTextBox.setFocus(true);
@@ -118,7 +111,6 @@ public class RegistrationLinkWithQRCodeDialog extends DataEntryDialog<Registrati
}
});
registrationLinkPanelContent.add(copyAnchor);
barcodePanel = new CaptionPanel(stringMessages.registrationLinkDialogQrcode());
barcodePanelContent = new VerticalPanel();
barcodePanel.add(barcodePanelContent);
@@ -144,7 +136,6 @@ public class RegistrationLinkWithQRCodeDialog extends DataEntryDialog<Registrati
}
final VerticalPanel dialogPanel = new VerticalPanel();
panel.add(dialogPanel);
dialogPanel.add(registrationLinkPanel);
dialogPanel.add(barcodePanel);
return panel;
@@ -155,29 +146,27 @@ public class RegistrationLinkWithQRCodeDialog extends DataEntryDialog<Registrati
Notification.notify(stringMessages.noEventSelected(), NotificationType.ERROR);
return;
}
String baseUrl = GWT.getHostPageBaseURL();
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.substring(0, baseUrl.lastIndexOf("/"));
baseUrl = baseUrl.substring(0, baseUrl.indexOf("/gwt"));
}
final String eventIdAsString = events.getValue().id.toString();
Map<String, String> parameters = new HashMap<String, String>();
parameters.put("regatta_name", regattaName);
parameters.put("secret", secret);
parameters.put("server", baseUrl);
parameters.put("event_id", eventIdAsString);
// TODO decide OPEN_REGATTA link variant based on invitationType
String deeplinkUrl = BranchIO.generateLink(BranchIOConstants.OPEN_REGATTA_2_APP_BRANCHIO, parameters,
URL::encodeQueryString);
urlTextBox.setText(deeplinkUrl);
if (invitationType != MailInvitationType.SailInsight2) {
if (invitationType != MailInvitationType.SailInsight2 &&
invitationType != MailInvitationType.SailInsight3) { // TODO it would be better to make it a property of an enum literal whether open regatta invitation is possible
getStatusLabel().setText(stringMessages.warningSailInsightVersion());
getStatusLabel().setStyleName("errorLabel");
}
sailingService.openRegattaRegistrationQrCode(deeplinkUrl, new AsyncCallback<String>() {
@Override
public void onFailure(Throwable caught) {
GWT.log("Qrcode generation failed: ", caught);
@@ -187,9 +176,7 @@ public class RegistrationLinkWithQRCodeDialog extends DataEntryDialog<Registrati
public void onSuccess(String result) {
GWT.log("Qrcode generated for url: " + deeplinkUrl);
qrCodeImage.setUrl("data:image/png;base64, " + result);
}
});
}
@@ -192,7 +192,13 @@ public class TaggingPanel extends ComponentWithoutSettings
tagButtons = new ArrayList<TagButton>();
taggingPanel = new DockLayoutPanel(Style.Unit.PX);
taggingPanel = new DockLayoutPanel(Style.Unit.PX) {
@Override
public void onResize() {
super.onResize();
taggingPanel.setWidgetSize(footerPanel, calculateFooterPanelHeight());
}
};
filterbarAndContentPanel = new HeaderPanel();
tagFooterPanel = new TagFooterPanel(this, sailingService, stringMessages, userService);
footerPanel = new ScrollPanel(tagFooterPanel);
@@ -705,15 +711,7 @@ public class TaggingPanel extends ComponentWithoutSettings
Scheduler.get().scheduleFinally(new RepeatingCommand() {
@Override
public boolean execute() {
// Default size of 2/3 the TaggingPanel
int height = (int) Math.round(taggingPanel.getOffsetHeight() / 3f * 2f);
// Get actual size of the footerPanel which might be 0 if the browser screwed up
final int vScroll = footerPanel.getWidget().getElement().getScrollHeight();
if (vScroll != 0) {
height = Math.min(height, vScroll);
}
height = Math.max(200, height);
taggingPanel.setWidgetSize(footerPanel, height);
taggingPanel.setWidgetSize(footerPanel, calculateFooterPanelHeight());
taggingPanel.animate(FOOTERPANEL_ANIMATION_PERIOD_MS);
return false;
}
@@ -728,6 +726,15 @@ public class TaggingPanel extends ComponentWithoutSettings
}
}
private int calculateFooterPanelHeight() {
// Start with footerPanel content height
int height = footerPanel.getWidget().getElement().getOffsetHeight();
// Limit size if we take up the entire height of the viewing area
// to make sure we don't get pushed out of it at the top making parts of the footerPanel inaccessible
height = Math.min(height, taggingPanel.getOffsetHeight());
return height;
}
/**
* Checks whether current user has permission to modify public tags.
*
@@ -40,7 +40,7 @@
-->
<system-property>
<name>webdriver.chrome.driver</name>
<value>C:\apps\ChromeSeleniumDriver\chromedriver.exe</value>
<value>/usr/bin/chromedriver</value>
</system-property>
<!--
You need geckodriver to be able to run Selenium tests on Firefox.
@@ -50,7 +50,7 @@
-->
<system-property>
<name>webdriver.gecko.driver</name>
<value>/path/to/geckodriver.exe</value>
<value>/usr/bin/geckodriver</value>
</system-property>
<!--
geckodriver automatically finds an installed version of Firefox.
@@ -86,12 +86,14 @@
The capabilities section can be used for fine tuning of the driver (e.g. version). For more informations see the
pages mentioned above.
-->
<!--
<!-- -->
<driver-definition class="org.openqa.selenium.firefox.FirefoxDriver">
</driver-definition>
-->
<!-- -->
<!--
<driver-definition class="org.openqa.selenium.chrome.ChromeDriver">
</driver-definition>
-->
<!--
<driver-definition class="org.openqa.selenium.ie.InternetExplorerDriver">
<capabilities>
@@ -26,21 +26,60 @@ public class LeaderboardApi {
private static final String SET_TRACKING_TIMES_URL = LEADERBOARDS_V1_RESOURCE_URL
+ "/{leaderboardname}/settrackingtimes";
private static final String START_TRACKING_URL = LEADERBOARDS_V1_RESOURCE_URL + "/{leaderboardname}/starttracking";
private static final String UPDATE_LEADERBOARD_URL = LEADERBOARDS_V1_RESOURCE_URL + "/{leaderboardname}/update";
private static final String PARAM_RESULT_DISCARDING_THRESHOLDS = "resultDiscardingThresholds";
private static final String PARAM_LEADERBOARD_DISPLAY_NAME = "leaderboardDisplayName";
public JSONArray getLeaderboards(ApiContext ctx) {
return ctx.get(LEADERBOARDS_LIST_URL);
}
public JSONObject getLeaderboard(ApiContext ctx, String leaderboardName) {
return ctx.get(toUrl(LEADERBOARD_URL, leaderboardName));
public Leaderboard getLeaderboard(ApiContext ctx, String leaderboardName) {
return new Leaderboard(ctx.get(toUrl(LEADERBOARD_URL, leaderboardName)));
}
public DeviceMappingRequest createDeviceMappingRequest(final ApiContext ctx, final String leaderboardName) {
return new DeviceMappingRequest(ctx, leaderboardName);
}
public TrackingTimes setTrackingTimes(ApiContext ctx, String leaderboardName, String raceColumnName, String fleetName,
Long startTime, Long endTime) {
public void updateLeaderboard(ApiContext ctx, String leaderboardName, String leaderboardDisplayName,
int[] resultDiscardingThresholds) {
JSONObject requestJson = new JSONObject();
JSONArray resultDiscardingThresholdsJsonArray = new JSONArray();
if (resultDiscardingThresholds != null) {
for (int i = 0; i < resultDiscardingThresholds.length; i++) {
resultDiscardingThresholdsJsonArray.add(resultDiscardingThresholds[i]);
}
}
requestJson.put(PARAM_RESULT_DISCARDING_THRESHOLDS,
resultDiscardingThresholds != null ? resultDiscardingThresholdsJsonArray : null);
requestJson.put(PARAM_LEADERBOARD_DISPLAY_NAME, leaderboardDisplayName);
ctx.post(toUrl(UPDATE_LEADERBOARD_URL, leaderboardName), null, requestJson);
}
public void updateLeaderboardDisplayName(ApiContext ctx, String leaderboardName, String leaderboardDisplayName) {
JSONObject requestJson = new JSONObject();
requestJson.put(PARAM_LEADERBOARD_DISPLAY_NAME, leaderboardDisplayName);
ctx.post(toUrl(UPDATE_LEADERBOARD_URL, leaderboardName), null, requestJson);
}
public void updateLeaderboardResultDiscardingThresholds(ApiContext ctx, String leaderboardName,
int[] resultDiscardingThresholds) {
JSONObject requestJson = new JSONObject();
JSONArray resultDiscardingThresholdsJsonArray = new JSONArray();
if (resultDiscardingThresholds != null) {
for (int i = 0; i < resultDiscardingThresholds.length; i++) {
resultDiscardingThresholdsJsonArray.add(resultDiscardingThresholds[i]);
}
}
requestJson.put(PARAM_RESULT_DISCARDING_THRESHOLDS,
resultDiscardingThresholds != null ? resultDiscardingThresholdsJsonArray : null);
ctx.post(toUrl(UPDATE_LEADERBOARD_URL, leaderboardName), null, requestJson);
}
public TrackingTimes setTrackingTimes(ApiContext ctx, String leaderboardName, String raceColumnName,
String fleetName, Long startTime, Long endTime) {
final Map<String, String> queryParams = new TreeMap<>();
queryParams.put("race_column", raceColumnName);
queryParams.put("fleet", fleetName);
@@ -63,6 +102,77 @@ public class LeaderboardApi {
return urlTemplate.replace("{leaderboardname}", leaderboardName);
}
public class Leaderboard extends JsonWrapper {
public Leaderboard(JSONObject json) {
super(json);
}
public String getName() {
return get("name");
}
public String getDisplayName() {
return get("displayName");
}
public Long getResultTimePoint() {
return get("resultTimepoint");
}
public String getResultState() {
return get("resultState");
}
public String getType() {
return get("type");
}
public String getShardingLeaderboardName() {
return get("shardingLeaderboardName");
}
public int[] getDiscardIndexResultsStartingWithHowManyRaces() {
final JSONArray resultDiscardingThresholdsRaw = (JSONArray) get(
"discardIndexResultsStartingWithHowManyRaces");
final int[] resultDiscardingThresholds = new int[resultDiscardingThresholdsRaw.size()];
for (int i = 0; i < resultDiscardingThresholdsRaw.size(); i++) {
resultDiscardingThresholds[i] = ((Long) resultDiscardingThresholdsRaw.get(i)).intValue();
}
return resultDiscardingThresholds;
}
public Boolean canBoatsOfCompetitorsChangePerRace() {
return get("canBoatsOfCompetitorsChangePerRace");
}
public Long getMaxCompetitorsCount() {
return get("maxCompetitorsCount");
}
public String getScoringComment() {
return get("scoringComment");
}
public Long getLastScoringUpdate() {
return get("lastScoringUpdate");
}
public String[] getColumnNames() {
final JSONArray columnNamesRaw = (JSONArray) get("columnNames");
final String[] columnNames = new String[columnNamesRaw.size()];
for (int i = 0; i < columnNamesRaw.size(); i++) {
columnNames[i] = (String) columnNamesRaw.get(i);
}
return columnNames;
}
public JSONArray getCompetitors() {
return get("competitors");
}
}
public class DeviceMappingRequest {
private final JSONObject requestBody = new JSONObject();
@@ -104,7 +104,7 @@ public class RegattaApi {
final String url = REGATTAS + "/" + regattaName + ADD_RACE_COLUMN_URL;
final Map<String, String> queryParams = new TreeMap<>();
queryParams.put("prefix", prefix);
queryParams.put("numberOfRaces", numberOfRaces != null ? numberOfRaces.toString() : null);
queryParams.put("numberofraces", numberOfRaces != null ? numberOfRaces.toString() : null);
final JSONArray json = ctx.post(url, queryParams);
final RaceColumn[] result = new RaceColumn[json.size()];
for (int i = 0; i < json.size(); i++) {
@@ -5,6 +5,7 @@ import static com.sap.sailing.selenium.api.core.ApiContext.SERVER_CONTEXT;
import static com.sap.sailing.selenium.api.core.ApiContext.createAdminApiContext;
import static java.lang.System.currentTimeMillis;
import static java.util.UUID.randomUUID;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -18,6 +19,7 @@ import com.sap.sailing.selenium.api.core.ApiContext;
import com.sap.sailing.selenium.api.event.EventApi;
import com.sap.sailing.selenium.api.event.LeaderboardApi;
import com.sap.sailing.selenium.api.event.LeaderboardApi.DeviceMappingRequest;
import com.sap.sailing.selenium.api.event.LeaderboardApi.Leaderboard;
import com.sap.sailing.selenium.api.event.LeaderboardApi.TrackingTimes;
import com.sap.sailing.selenium.api.regatta.Competitor;
import com.sap.sailing.selenium.api.regatta.RaceColumn;
@@ -42,19 +44,18 @@ public class LeaderboardApiTest extends AbstractSeleniumTest {
public void testGetLeaderboardForCreatedEvent() {
final ApiContext ctx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
eventApi.createEvent(ctx, LEADERBOARD_NAME, BOATCLASSNAME, CLOSED, "default");
JSONObject leaderBoard = leaderboardApi.getLeaderboard(ctx, LEADERBOARD_NAME);
assertEquals("read: leaderboard.name is different", LEADERBOARD_NAME, leaderBoard.get("name"));
assertEquals("read: leaderboard.displayName is different", LEADERBOARD_NAME, leaderBoard.get("displayName"));
assertNotNull("read: leaderboard.resultTimepoint is missing", leaderBoard.get("resultTimepoint"));
assertEquals("read: leaderboard.resultState is different", "Live", leaderBoard.get("resultState"));
assertEquals("read: leaderboard.type is different", "RegattaLeaderboard", leaderBoard.get("type"));
Leaderboard leaderBoard = leaderboardApi.getLeaderboard(ctx, LEADERBOARD_NAME);
assertEquals("read: leaderboard.name is different", LEADERBOARD_NAME, leaderBoard.getName());
assertEquals("read: leaderboard.displayName is different", LEADERBOARD_NAME, leaderBoard.getDisplayName());
assertNotNull("read: leaderboard.resultTimepoint is missing", leaderBoard.getResultTimePoint());
assertEquals("read: leaderboard.resultState is different", "Live", leaderBoard.getResultState());
assertEquals("read: leaderboard.type is different", "RegattaLeaderboard", leaderBoard.getType());
assertEquals("read: leaderboard.canBoatsOfCompetitorsChangePerRace is different", false,
leaderBoard.get("canBoatsOfCompetitorsChangePerRace"));
assertNull("read: leaderboard.maxCompetitorsCount should be null", leaderBoard.get("maxCompetitorsCount"));
assertNull("read: leaderboard.scoringComment should be null", leaderBoard.get("scoringComment"));
assertNull("read: leaderboard.lastScoringUpdate should be null", leaderBoard.get("lastScoringUpdate"));
assertEquals("read: leaderboard.columnNames should be empty", 0,
((JSONArray) leaderBoard.get("columnNames")).size());
leaderBoard.canBoatsOfCompetitorsChangePerRace());
assertNull("read: leaderboard.maxCompetitorsCount should be null", leaderBoard.getMaxCompetitorsCount());
assertNull("read: leaderboard.scoringComment should be null", leaderBoard.getScoringComment());
assertNull("read: leaderboard.lastScoringUpdate should be null", leaderBoard.getLastScoringUpdate());
assertEquals("read: leaderboard.columnNames should be empty", 0, leaderBoard.getColumnNames().length);
assertEquals("read: leaderboard.competitors should be empty", 0,
((JSONArray) leaderBoard.get("competitors")).size());
assertEquals("read: leaderboard.ShardingLeaderboardName is different",
@@ -62,6 +63,90 @@ public class LeaderboardApiTest extends AbstractSeleniumTest {
leaderBoard.get("ShardingLeaderboardName"));
}
@Test
public void testUpdatingLeaderboard() {
final ApiContext ctx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
eventApi.createEvent(ctx, LEADERBOARD_NAME, BOATCLASSNAME, CLOSED, "default");
final Leaderboard leaderboardOriginal = leaderboardApi.getLeaderboard(ctx, LEADERBOARD_NAME);
// updating resultDiscardingThresholds
final int[] newResultDiscardingThresholds = new int[] { 1, 2, 3, 4, 5 };
leaderboardApi.updateLeaderboard(ctx, LEADERBOARD_NAME, leaderboardOriginal.getDisplayName(),
newResultDiscardingThresholds);
// reloading and check if updated
Leaderboard leaderboardReloaded = leaderboardApi.getLeaderboard(ctx, LEADERBOARD_NAME);
assertArrayEquals(newResultDiscardingThresholds,
leaderboardReloaded.getDiscardIndexResultsStartingWithHowManyRaces());
// remove and check again
final int[] newEmptyResultDiscardingThresholds = new int[] {};
leaderboardApi.updateLeaderboard(ctx, LEADERBOARD_NAME, leaderboardOriginal.getDisplayName(),
newEmptyResultDiscardingThresholds);
leaderboardReloaded = leaderboardApi.getLeaderboard(ctx, LEADERBOARD_NAME);
assertArrayEquals(newEmptyResultDiscardingThresholds,
leaderboardReloaded.getDiscardIndexResultsStartingWithHowManyRaces());
// passing null should not change anything
leaderboardApi.updateLeaderboard(ctx, LEADERBOARD_NAME, leaderboardOriginal.getDisplayName(),
newResultDiscardingThresholds);
leaderboardReloaded = leaderboardApi.getLeaderboard(ctx, LEADERBOARD_NAME);
assertArrayEquals(newResultDiscardingThresholds,
leaderboardReloaded.getDiscardIndexResultsStartingWithHowManyRaces());
leaderboardApi.updateLeaderboard(ctx, LEADERBOARD_NAME, leaderboardOriginal.getDisplayName(), null);
assertArrayEquals(newResultDiscardingThresholds,
leaderboardReloaded.getDiscardIndexResultsStartingWithHowManyRaces());
// check that no other atributes have been changed
assertEquals(leaderboardOriginal.getName(), leaderboardReloaded.getName());
assertEquals(leaderboardOriginal.getDisplayName(), leaderboardReloaded.getDisplayName());
// update leaderboardDisplayName
leaderboardApi.updateLeaderboard(ctx, LEADERBOARD_NAME, "another display name", null);
leaderboardReloaded = leaderboardApi.getLeaderboard(ctx, LEADERBOARD_NAME);
assertEquals("another display name", leaderboardReloaded.getDisplayName());
// check that no other atributes have been changed
assertEquals(leaderboardOriginal.getName(), leaderboardReloaded.getName());
assertArrayEquals(newResultDiscardingThresholds,
leaderboardReloaded.getDiscardIndexResultsStartingWithHowManyRaces());
// update leaderboardDisplayName and resultDiscardingThresholds
final int[] newResultDiscardingThresholds2 = new int[] {7, 8};
leaderboardApi.updateLeaderboard(ctx, LEADERBOARD_NAME, "another display name 2", newResultDiscardingThresholds2);
leaderboardReloaded = leaderboardApi.getLeaderboard(ctx, LEADERBOARD_NAME);
assertEquals("another display name 2", leaderboardReloaded.getDisplayName());
assertArrayEquals(newResultDiscardingThresholds2, newResultDiscardingThresholds2);
// update leaderboardDisplayName to null, this result in the name of the leaderboard
leaderboardApi.updateLeaderboard(ctx, LEADERBOARD_NAME, /* leaderboardDisplayName */ null, newResultDiscardingThresholds2);
leaderboardReloaded = leaderboardApi.getLeaderboard(ctx, LEADERBOARD_NAME);
// if display name is null, the name of the leaderboard is returned
assertEquals(leaderboardReloaded.getName(), leaderboardReloaded.getDisplayName());
}
@Test
public void testUpdatingLeaderboardDisplayNameToNull() {
final ApiContext ctx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
eventApi.createEvent(ctx, LEADERBOARD_NAME, BOATCLASSNAME, CLOSED, "default");
// update to not null values
final int[] newResultDiscardingThresholds = new int[] { 1, 2, 3, 4, 5 };
leaderboardApi.updateLeaderboard(ctx, LEADERBOARD_NAME, "newLeaderboardDisplayName",
newResultDiscardingThresholds);
Leaderboard leaderboardReloaded = leaderboardApi.getLeaderboard(ctx, LEADERBOARD_NAME);
assertEquals("newLeaderboardDisplayName", leaderboardReloaded.getDisplayName());
assertArrayEquals(newResultDiscardingThresholds,
leaderboardReloaded.getDiscardIndexResultsStartingWithHowManyRaces());
// update to display name to null
leaderboardApi.updateLeaderboardDisplayName(ctx, LEADERBOARD_NAME, null);
leaderboardReloaded = leaderboardApi.getLeaderboard(ctx, LEADERBOARD_NAME);
assertEquals(LEADERBOARD_NAME, leaderboardReloaded.getDisplayName());
assertArrayEquals(newResultDiscardingThresholds,
leaderboardReloaded.getDiscardIndexResultsStartingWithHowManyRaces());
}
@Test
public void testDeviceMappingStartAndEnd() throws Exception {
final ApiContext ctx = createAdminApiContext(getContextRoot(), SERVER_CONTEXT);
@@ -159,15 +159,14 @@ public class RegattaApiTest extends AbstractSeleniumTest {
RaceColumn[] result = regattaApi.addRaceColumn(ctx, EVENT_NAME, "T", 5);
assertEquals("read: racecolumn.seriesname is different", "Default", result[0].getSeriesName());
assertEquals("read: racecolumn.racename is different", "T1", result[0].getRaceName());
// assertEquals("read: racecolumn.seriesname is different", "Default", result[4].getSeriesName());
// assertEquals("read: racecolumn.racename is different", "T5", result[4].getRaceName());
assertEquals("read: racecolumn.seriesname is different", "Default", result[4].getSeriesName());
assertEquals("read: racecolumn.racename is different", "T5", result[4].getRaceName());
}
@Test
public void testRegisterExistingCompetitorWithSecret() {
final AdminConsolePage adminConsole = goToPage(getWebDriver(), getContextRoot());
adminConsole.goToLocalServerPanel().setSelfServiceServer(true);
SecurityApi securityApi = new SecurityApi();
final ApiContext adminSecurityCtx = createAdminApiContext(getContextRoot(), ApiContext.SECURITY_CONTEXT);
securityApi.createUser(adminSecurityCtx, "donald", "Donald Duck", null, "daisy0815");
@@ -176,22 +175,17 @@ public class RegattaApiTest extends AbstractSeleniumTest {
ApiContext.SECURITY_CONTEXT, "donald", "daisy0815");
securityApi.createUser(adminSecurityCtx, "dagobert", "Dagobert Duck", null, "daisy1337");
final ApiContext userOwningEventCtx = createApiContext(getContextRoot(), SERVER_CONTEXT, "dagobert", "daisy1337");
// TODO we currently can't just create a competitor using the API. This is only possible by using a temporary Event/Regatta.
eventApi.createEvent(userWithCompetitorCtx, EVENT_NAME, BOAT_CLASS, CLOSED, "default");
Competitor competitor = regattaApi.createAndAddCompetitor(userWithCompetitorCtx, EVENT_NAME, BOAT_CLASS, null, "donald", "USA");
// ensure the reader can actually see the competitor
UserGroup group = usergroupApi.getUserGroupByName(userWithCompetitorAndSecurityCtx, "donald"+SecurityService.TENANT_SUFFIX);
usergroupApi.addRoleToGroup(userWithCompetitorAndSecurityCtx, group.getGroupId(),
UUID.fromString(/* sailing viewer role id */"c42948df-517b-45cb-9fa9-d1e79f18e115"), true);
// This is the event to register the existing competitor for
Event eventToRegisterExistingCompetitor = eventApi.createEvent(userOwningEventCtx, OTHER_EVENT_NAME, BOAT_CLASS, CompetitorRegistrationType.OPEN_UNMODERATED, "default");
regattaApi.addCompetitor(userWithCompetitorCtx, OTHER_EVENT_NAME, competitor.getId(), Optional.of(eventToRegisterExistingCompetitor.getSecret()));
Competitor[] competitors = regattaApi.getCompetitors(userOwningEventCtx, OTHER_EVENT_NAME);
assertEquals(1, competitors.length);
assertEquals(competitor.getId(), competitors[0].getId());
}
@@ -124,6 +124,7 @@ import com.sap.sailing.server.hierarchy.SailingHierarchyOwnershipUpdater;
import com.sap.sailing.server.interfaces.RacingEventService;
import com.sap.sailing.server.operationaltransformation.RemoveAndUntrackRace;
import com.sap.sailing.server.operationaltransformation.StopTrackingRace;
import com.sap.sailing.server.operationaltransformation.UpdateLeaderboard;
import com.sap.sailing.server.security.PermissionAwareRaceTrackingHandler;
import com.sap.sse.InvalidDateException;
import com.sap.sse.common.Bearing;
@@ -268,6 +269,45 @@ public class LeaderboardsResource extends AbstractLeaderboardsResource {
return jsonLeaderboard;
}
@POST
@Path("{name}/update")
@Consumes(MediaType.APPLICATION_JSON)
@Produces("application/json;charset=UTF-8")
public Response updateLeaderboard(@PathParam("name") String leaderboardName, String json)
throws ParseException, JsonDeserializationException {
final Object requestBody = JSONValue.parseWithException(json);
final JSONObject requestObject = Helpers.toJSONObjectSafe(requestBody);
final Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName);
if (leaderboard != null) {
SecurityUtils.getSubject().checkPermission(
SecuredDomainType.LEADERBOARD.getStringPermissionForObject(DefaultActions.UPDATE, leaderboard));
String newLeaderboardDisplayName = leaderboard.getDisplayName();
if (requestObject.containsKey("leaderboardDisplayName")) {
newLeaderboardDisplayName = (String) requestObject.get("leaderboardDisplayName");
}
int[] resultDiscardingThresholds = null;
if (requestObject.containsKey("resultDiscardingThresholds")) {
final JSONArray resultDiscardingThresholdsRaw = (JSONArray) requestObject
.get("resultDiscardingThresholds");
if (resultDiscardingThresholdsRaw != null) {
resultDiscardingThresholds = new int[resultDiscardingThresholdsRaw.size()];
for (int i = 0; i < resultDiscardingThresholdsRaw.size(); i++) {
resultDiscardingThresholds[i] = ((Long) resultDiscardingThresholdsRaw.get(i)).intValue();
}
}
}
getService().apply(new UpdateLeaderboard(/* leaderboardName */ leaderboard.getName(),
/* newLeaderboardName */ leaderboard.getName(), newLeaderboardDisplayName,
resultDiscardingThresholds,
leaderboard.getDefaultCourseArea() != null ? leaderboard.getDefaultCourseArea().getId() : null));
} else {
return Response.status(Status.NOT_FOUND).entity(
"Could not find a leaderboard with name '" + StringEscapeUtils.escapeHtml(leaderboardName) + "'.")
.type(MediaType.TEXT_PLAIN).build();
}
return Response.ok().header("Content-Type", MediaType.APPLICATION_JSON + ";charset=UTF-8").build();
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Path("{name}/device_mappings/start")
@@ -189,6 +189,10 @@ name to make sure you get to the event you're interested in.
<td><a href="leaderboardGetDoc.html">/api/v1/leaderboards/{name}</a></td>
<td>Gets the details of a leaderboard</td>
</tr>
<tr>
<td><a href="leaderboardUpdateDoc.html">/api/v1/leaderboards/{name}/update</a></td>
<td>Modifies attributes of a leaderboard</td>
</tr>
<tr>
<td><a href="leaderboardCheckinPostDoc.html">/api/v1/leaderboards/{name}/device_mappings/start</a></td>
<td>Tracking App Device Checkin on leaderboard</td>
@@ -0,0 +1,68 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<link href='../../../../sailing-fontface-1.0.cache.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sailing Analytics Webservices API Version 1.0</title>
</head>
<body>
<h1>SAP Sailing Analytics Webservices API Version 1.0</h1>
<h2>URL: /api/v1/leaderboard/{name}/update</h2>
<b>Description:</b>
<p>The service can modify some of the details of a leaderboard. Currently this allows to update the discarding thresholds of a leaderboard and the leaderboard display name.</p>
<br/>
<table>
<tr>
<td>Webservice Type:</td>
<td>REST</td>
</tr>
<tr>
<td>Output format:</td>
<td>Json</td>
</tr>
<tr>
<td>Mandatory parameters:</td>
<td>None</td>
</tr>
<tr>
<td>Optional parameters:</td>
<td>None</td>
</tr>
<tr>
<td>Request method:</td>
<td>POST</td>
</tr>
<tr>
<td>Payload:</td>
<td>format: json<br /> *(optional) resultDiscardingThresholds: if
parameter is absent or value is null, the discarding thresholds will
not change. If passing an empty array the discarding thresholds
will be cleared.<br/>
*(optional) leaderboardDisplayName: if parameter is absent
the displayName will not change.
</td>
</tr>
<tr>
<td>Example:</td>
<td><a
href="http://www.sapsailing.com/sailingserver/api/v1/leaderboards/505 Worlds 2013/update">http://www.sapsailing.com/sailingserver/api/v1/leaderboards/505
Worlds 2013/update</a><br/>
Request:
<pre>
{
"resultDiscardingThresholds":[1,2],
"leaderboardDisplayName":"505 Worlds 2014"
}
</pre>
Response: Status 200
</tr>
</table>
<div style="height: 1em;"></div>
<a href="index.html">Back to Web Service Overview</a>
</body>
</html>
@@ -35,6 +35,7 @@ public interface AccessControlStore extends Named {
void removeAclDenial(QualifiedObjectIdentifier idOfAccessControlledObject, UserGroup userGroup, String action);
void denyAclPermission(QualifiedObjectIdentifier idOfAccessControlledObject, UserGroup userGroup, String action);
void removeAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject);
Iterable<OwnershipAnnotation> getOwnerships();
@@ -50,9 +51,11 @@ public interface AccessControlStore extends Named {
OwnershipAnnotation setOwnership(QualifiedObjectIdentifier idOfOwnedObject, User userOwner, UserGroup tenantOwner,
String displayNameOfOwnedObject);
void removeOwnership(QualifiedObjectIdentifier idOfOwnedObject);
void clear();
void replaceContentsFrom(AccessControlStore newAccessControlStore);
/**
@@ -76,3 +76,4 @@ bearerToken = com.sap.sse.security.BearerTokenOrBasicOrFormAuthenticationFilter
/api/restsecurity/usergroup = bearerToken
/api/restsecurity/role = bearerToken
/api/restsecurity/role/** = bearerToken
/api/restsecurity/ownership/** = bearerToken
@@ -93,6 +93,9 @@ public interface SecurityService extends ReplicableWithObjectInputStream<Replica
AccessControlList overrideAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject,
Map<UserGroup, Set<String>> permissionMap);
AccessControlList overrideAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject,
Map<UserGroup, Set<String>> permissionMap, String displayNameOfAccessControlledObject);
/**
* @param name The name of the user group to add
*/
@@ -0,0 +1,24 @@
package com.sap.sse.security.exceptions;
import javax.ws.rs.core.Response.Status;
import com.sap.sse.security.jaxrs.api.OwnershipResource;
/***
* OwnershipException is used for {@link OwnershipResource} class to handle any exception in ownership Api's.
*
* @author Usman Ali
*
*/
public class OwnershipException extends Exception {
private static final long serialVersionUID = -7464597099374135405L;
private Status status;
public OwnershipException(String message, Status status) {
super(message);
this.status = status;
}
public Status getStatus() {
return status;
}
}
@@ -0,0 +1,28 @@
package com.sap.sse.security.exceptions.handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import com.sap.sse.security.exceptions.OwnershipException;
import com.sap.sse.security.jaxrs.ShiroAuthorizationExceptionTo401ResponseMapper;
import com.sap.sse.security.model.GeneralResponse;
/***
* This class is used to handle {@link OwnershipException} thrown from any API of project.
* @author Usman Ali
*
*/
@Provider
public class OwnershipExceptionHandler implements ExceptionMapper<OwnershipException> {
private static final Logger logger = Logger.getLogger(ShiroAuthorizationExceptionTo401ResponseMapper.class.getName());
@Override
public Response toResponse(OwnershipException exception) {
logger.log(Level.WARNING, "OwnershipException occurred in REST call: " + exception.getMessage());
return Response.status(exception.getStatus()).entity(new GeneralResponse(false, exception.getMessage()).toString()).build();
}
}
@@ -140,7 +140,6 @@ public class Activator implements BundleActivator {
waitForUserStoreService(bundleContext);
}
context.registerService(ClearStateTestSupport.class, new ClearStateTestSupport() {
@Override
public void clearState() throws Exception {
Activator.this.clearState();
@@ -149,7 +148,7 @@ public class Activator implements BundleActivator {
}
/**
* This method will clean the userStore and AccessControllStore, and reset the securityService. It will then
* This method will clean the userStore and AccessControlStore, and reset the securityService. It will then
* reinitialize them in the same fashion an empty server will do.
*/
protected void clearState() throws InterruptedException, UserGroupManagementException, UserManagementException {
@@ -523,6 +523,14 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
return updateAccessControlList(idOfAccessControlledObject, permissionMap);
}
@Override
public AccessControlList overrideAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject,
Map<UserGroup, Set<String>> permissionMap, String displayNameOfAccessControlledObject) {
accessControlStore.removeAccessControlList(idOfAccessControlledObject);
accessControlStore.setEmptyAccessControlList(idOfAccessControlledObject, displayNameOfAccessControlledObject);
return updateAccessControlList(idOfAccessControlledObject, permissionMap);
}
@Override
public AccessControlList updateAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject,
Map<UserGroup, Set<String>> permissionMap) {
@@ -0,0 +1,186 @@
package com.sap.sse.security.jaxrs.api;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.shiro.SecurityUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import com.sap.sse.security.exceptions.OwnershipException;
import com.sap.sse.security.jaxrs.AbstractSecurityResource;
import com.sap.sse.security.model.GeneralResponse;
import com.sap.sse.security.shared.AccessControlListAnnotation;
import com.sap.sse.security.shared.HasPermissions.DefaultActions;
import com.sap.sse.security.shared.OwnershipAnnotation;
import com.sap.sse.security.shared.QualifiedObjectIdentifier;
import com.sap.sse.security.shared.TypeRelativeObjectIdentifier;
import com.sap.sse.security.shared.impl.QualifiedObjectIdentifierImpl;
import com.sap.sse.security.shared.impl.User;
import com.sap.sse.security.shared.impl.UserGroup;
@Path("/restsecurity/ownership")
public class OwnershipResource extends AbstractSecurityResource {
private static final String KEY_OBJECT_ID = "objectId";
private static final String KEY_OBJECT_TYPE = "objectType";
private static final String KEY_USERNAME = "username";
private static final String KEY_GROUP_ID = "groupId";
private static final String KEY_ACL = "acl";
private static final String KEY_ACTIONS = "actions";
private static final String KEY_DISPLAY_NAME = "displayName";
@Path("{objectType}/{typeRelativeObjectId}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces("application/json;charset=UTF-8")
public Response setOwnership(@PathParam("objectType") String objectType,
@PathParam("typeRelativeObjectId") String typeRelativeObjectId, String jsonBody) throws OwnershipException {
final JSONObject json = (JSONObject) JSONValue.parse(jsonBody);
QualifiedObjectIdentifier identifier = new QualifiedObjectIdentifierImpl((String) json.get(KEY_OBJECT_TYPE),
new TypeRelativeObjectIdentifier((String) json.get(KEY_OBJECT_ID)));
try {
SecurityUtils.getSubject().checkPermission(identifier.getStringPermission(DefaultActions.CHANGE_OWNERSHIP));
} catch (Exception ex) {
throw new OwnershipException("Not permitted to change ownership.", Status.FORBIDDEN);
}
final OwnershipAnnotation existingOwnership = getService().getOwnership(identifier);
final User user;
if (json.containsKey(KEY_USERNAME)) {
final String username = (String) json.get(KEY_USERNAME);
if (username == null) {
user = null;
} else {
user = getService().getUserByName(username);
if (user == null) {
// username provided but cannot be resolved
throw new OwnershipException("User Not found", Status.BAD_REQUEST);
}
}
} else {
user = existingOwnership == null ? null : existingOwnership.getAnnotation().getUserOwner();
}
final UserGroup userGroup;
if (json.containsKey(KEY_GROUP_ID)) {
final String userGroupIdAsString = (String) json.get(KEY_GROUP_ID);
if (userGroupIdAsString == null) {
userGroup = null;
} else {
userGroup = getService().getUserGroup(UUID.fromString(userGroupIdAsString));
if (userGroup == null) {
throw new OwnershipException("UserGroup Not found", Status.BAD_REQUEST);
}
}
} else {
userGroup = existingOwnership == null ? null : existingOwnership.getAnnotation().getTenantOwner();
}
if (json.containsKey(KEY_DISPLAY_NAME)) {
getService().setOwnership(identifier, user, userGroup, (String) json.get(KEY_DISPLAY_NAME));
} else {
getService().setOwnership(identifier, user, userGroup);
}
return Response.ok(new GeneralResponse(true, "Ownership changed successfully").toString()).build();
}
@Path("{objectType}/{typeRelativeObjectId}")
@GET
@Produces("application/json;charset=UTF-8")
public Response getOwnership(@PathParam("objectType") String objectType,
@PathParam("typeRelativeObjectId") String typeRelativeObjectId) throws OwnershipException {
QualifiedObjectIdentifier identifier = new QualifiedObjectIdentifierImpl(objectType,
new TypeRelativeObjectIdentifier(typeRelativeObjectId));
final OwnershipAnnotation existingOwnership = getService().getOwnership(identifier);
final JSONObject result = new JSONObject();
result.put(KEY_OBJECT_TYPE, objectType);
result.put(KEY_OBJECT_ID, typeRelativeObjectId);
result.put(KEY_GROUP_ID, existingOwnership == null || existingOwnership.getAnnotation().getTenantOwner() == null ? null : existingOwnership.getAnnotation().getTenantOwner().getId().toString());
result.put(KEY_USERNAME, existingOwnership == null || existingOwnership.getAnnotation().getUserOwner() == null ? null : existingOwnership.getAnnotation().getUserOwner().getName());
result.put(KEY_DISPLAY_NAME, existingOwnership == null || existingOwnership.getDisplayNameOfAnnotatedObject() == null ? null : existingOwnership.getDisplayNameOfAnnotatedObject());
return Response.ok(result.toJSONString()).build();
}
@Path("{objectType}/{typeRelativeObjectId}/acl")
@GET
@Produces("application/json;charset=UTF-8")
public Response getAccessControlLists(@PathParam("objectType") String objectType,
@PathParam("typeRelativeObjectId") String typeRelativeObjectId) throws OwnershipException {
QualifiedObjectIdentifier identifier = new QualifiedObjectIdentifierImpl(objectType,
new TypeRelativeObjectIdentifier(typeRelativeObjectId));
final AccessControlListAnnotation acl = getService().getAccessControlList(identifier);
final JSONObject result = new JSONObject();
result.put(KEY_OBJECT_TYPE, objectType);
result.put(KEY_OBJECT_ID, typeRelativeObjectId);
result.put(KEY_DISPLAY_NAME, acl == null || acl.getDisplayNameOfAnnotatedObject() == null ? null : acl.getDisplayNameOfAnnotatedObject());
final JSONArray actionsByUserGroup = new JSONArray();
result.put(KEY_ACL, actionsByUserGroup);
if (acl != null && acl.getAnnotation() != null) {
for (final Entry<UserGroup, Set<String>> e : acl.getAnnotation().getActionsByUserGroup().entrySet()) {
final JSONObject actionsForGroup = new JSONObject();
actionsByUserGroup.add(actionsForGroup);
actionsForGroup.put(KEY_GROUP_ID, e.getKey() == null ? null : e.getKey().getId().toString());
final JSONArray actions = new JSONArray();
actions.addAll(e.getValue());
actionsForGroup.put(KEY_ACTIONS, actions);
}
}
return Response.ok(result.toJSONString()).build();
}
@Path("{objectType}/{typeRelativeObjectId}/acl")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces("application/json;charset=UTF-8")
public Response getAccessControlLists(@PathParam("objectType") String objectType,
@PathParam("typeRelativeObjectId") String typeRelativeObjectId, String jsonBody) throws OwnershipException {
final JSONObject json = (JSONObject) JSONValue.parse(jsonBody);
final JSONArray actionsByUserGroupJson = (JSONArray) json.get(KEY_ACL);
QualifiedObjectIdentifier identifier = new QualifiedObjectIdentifierImpl(objectType,
new TypeRelativeObjectIdentifier(typeRelativeObjectId));
try {
SecurityUtils.getSubject().checkPermission(identifier.getStringPermission(DefaultActions.CHANGE_ACL));
} catch (Exception ex) {
throw new OwnershipException("Not permitted to change ownership.", Status.FORBIDDEN);
}
final Map<UserGroup, Set<String>> actionsByUserGroup = new HashMap<>();
for (final Object o : actionsByUserGroupJson) {
final JSONObject permissionsForGroup = (JSONObject) o;
final String groupIdAsString = (String) permissionsForGroup.get(KEY_GROUP_ID);
final UserGroup group;
if (groupIdAsString == null) {
group = null;
} else {
group = getService().getUserGroup(UUID.fromString(groupIdAsString));
if (group == null) {
throw new OwnershipException(String.format("UserGroup with ID %s Not found", groupIdAsString), Status.BAD_REQUEST);
}
}
final JSONArray actions = (JSONArray) permissionsForGroup.get(KEY_ACTIONS);
final Set<String> actionsForGroup = new HashSet<>();
for (final Object action : actions) {
actionsForGroup.add(action.toString());
}
actionsByUserGroup.put(group, actionsForGroup);
}
if (json.containsKey(KEY_DISPLAY_NAME)) {
final String displayName = (String) json.get(KEY_DISPLAY_NAME);
getService().overrideAccessControlList(identifier, actionsByUserGroup, displayName);
} else {
getService().overrideAccessControlList(identifier, actionsByUserGroup);
}
return Response.ok(new GeneralResponse(true, "ACL changed successfully").toString()).build();
}
}
@@ -5,6 +5,7 @@ import java.util.Set;
import javax.ws.rs.core.Application;
import com.sap.sse.security.exceptions.handler.OwnershipExceptionHandler;
import com.sap.sse.security.jaxrs.ShiroAuthorizationExceptionTo401ResponseMapper;
public class RestApiApplication extends Application {
@@ -13,9 +14,10 @@ public class RestApiApplication extends Application {
classes.add(SecurityResource.class);
classes.add(UserGroupResource.class);
classes.add(RoleResource.class);
classes.add(OwnershipResource.class);
// exception mapper
classes.add(ShiroAuthorizationExceptionTo401ResponseMapper.class);
classes.add(OwnershipExceptionHandler.class);
return classes;
}
}
@@ -268,19 +268,17 @@ public class UserGroupResource extends AbstractSecurityResource {
public Response addRoleToUserGroup(@PathParam(KEY_GROUP_ID) String userGroupId,
@PathParam(KEY_ROLE_ID) String roleIdString, String body) {
Response response;
try {
final UUID groupId = UUID.fromString(userGroupId);
final UserGroup usergroup = getService().getUserGroup(groupId);
final UUID roleId = UUID.fromString(roleIdString);
final UserGroup usergroup = getService().getUserGroup(groupId);
if (usergroup == null) {
response = Response.status(Status.BAD_REQUEST).entity("Usergroup with this name does not exist.")
.build();
response = Response.status(Status.BAD_REQUEST).entity(
String.format("Usergroup with ID %s does not exist.", userGroupId)).build();
} else {
final RoleDefinition role = getService().getRoleDefinition(roleId);
if (role == null) {
response = Response.status(Status.BAD_REQUEST).entity("Role with this id does not exist.").build();
response = Response.status(Status.BAD_REQUEST).entity(String.format("Role with ID %s does not exist.", roleId)).build();
} else {
if (getService().hasCurrentUserUpdatePermission(usergroup)
&& getService().hasCurrentUserReadPermission(role)) {
@@ -291,15 +289,16 @@ public class UserGroupResource extends AbstractSecurityResource {
if (!getService().hasCurrentUserMetaPermissionsOfRoleDefinitionWithQualification(role,
new Ownership(null, usergroup))) {
response = Response.status(Status.UNAUTHORIZED)
.entity(String.format("Not permitted to add role %s to group", role.getName()))
.build();
.entity(String.format("Not permitted to add role %s to group %s",
role.getName(), usergroup.getName())).build();
} else {
getService().putRoleDefinitionToUserGroup(usergroup, role, roleForAll);
response = Response.ok().build();
}
} else {
response = Response.status(Status.BAD_REQUEST)
.entity("Role was already added to the group.").build();
.entity(String.format("Role %s was already added to group %s.", role.getName(),
usergroup.getName())).build();
}
} else {
response = Response.status(Status.UNAUTHORIZED).build();
@@ -0,0 +1,43 @@
package com.sap.sse.security.model;
import java.io.Serializable;
/***
* This Class will be used to send general response in JSON format.
* @author Usman Ali
*
*/
public class GeneralResponse implements Serializable{
private static final long serialVersionUID = 1L;
private boolean responseStatus;
private String responseMessage;
public GeneralResponse(boolean responseStatus, String responseMessage) {
super();
this.responseStatus = responseStatus;
this.responseMessage = responseMessage;
}
public boolean isResponseStatus() {
return responseStatus;
}
public void setResponseStatus(boolean responseStatus) {
this.responseStatus = responseStatus;
}
public String getResponseMessage() {
return responseMessage;
}
public void setResponseMessage(String responseMessage) {
this.responseMessage = responseMessage;
}
@Override
public String toString() {
return " {\"responseStatus\":\"" + responseStatus + "\", \"responseMessage\":\"" + responseMessage + "\"}";
}
}
@@ -62,6 +62,10 @@ be <code>http://sapsailing.com/security/api/restsecurity/login</code>.
<td><a href="remove_access_token.html">/api/restsecurity/remove_access_token</a></td>
<td>Remove an existing access token, rendering it invalid for authenticating to the server</td>
</tr>
<tr>
<td><a href="ownership.html">/api/restsecurity/ownership</a></td>
<td>Change Object ownership</td>
</tr>
</tbody>
</table>
</body>
@@ -0,0 +1,124 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<link href='../../../../fontface.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link href='../../../main.css' media='screen' rel='stylesheet' title='Default stylesheet' type='text/css' />
<link rel='icon' href='../../../../sap.ico' type='image/x-icon'>
<title>SAP Sponsorships Engine Security Webservices API</title>
</head>
<body>
<h1>SAP Sponsorships Engine Security Webservices API</h1>
<h2>URL: /api/restsecurity/onwership</h2>
<b>Description:</b>
<p>The service obtains or changes the ownership of object, or obtains or changes access control list (ACL) properties of an object.</p>
<br/>
<b>Change Ownership</b>
<table>
<tr>
<td>Request Method:</td>
<td>PUT /{objectType}/{typeRelativeObjectId}</td>
</tr>
<tr>
<td>Output format:</td>
<td>a JSON document of the following format<br>
<pre>{ "responseStatus": "true", "responseMessage": "Ownership changed successfully"}</pre>
</td>
</tr>
<tr>
<td>Parameter (path and body):</td>
<td>
<div>typeRelativeObjectId</div> Unique Object id.
<div>objectType</div> Object type represents the type of object to change the ownership.
<div>groupId</div> GroupId to assign given group to ownership of object object. If the property is missing in the body,
the group ownership is not touched. If it is null, the group ownership is reset to null.
<div>username</div> Username to search the user in order to attach it with given Object ownership. If the property is missing in the body,
the user ownership is not touched. If it is null, the user ownership is reset to null.
</td>
</tr>
<tr>
<td>Example:</td>
<td><code>curl -X POST -d '{
"groupId":"admin-tenant",
"username":"admin"
}' -H 'Content-type: application/json' "http://admin:admin@127.0.0.1:8888/security/api/restsecurity/ownership/USER_GROUP/427ebd42-ac69-4f74-a171-376d9bea9457"</code><br>
Will provide a JSON document as explained above.</td>
</tr>
<tr>
<td>Request Method:</td>
<td>GET /{objectType}/{typeRelativeObjectId}</td>
</tr>
<tr>
<td>Output format:</td>
<td>a JSON document of the following format<br>
<pre>{"objectType":"USER_GROUP","objectId":"82832851-07ac-47ee-9ddf-6f4f9eaa7823","groupId":"82832851-07ac-47ee-9ddf-6f4f9eaa7823","username":"admin"}</pre>
</td>
</tr>
<tr>
<td>Parameter (path)</td>
<td>
<div>objectType</div> Object type represents the type of object to change the ownership.
<div>objectId</div> Unique Object id.
</td>
</tr>
<tr>
<td>Example:</td>
<td><code>curl "http://127.0.0.1:8888/security/api/restsecurity/ownership/USER_GROUP/82832851-07ac-47ee-9ddf-6f4f9eaa7823"</code><br>
Will provide a JSON document as explained above.</td>
</tr>
<tr>
<td>Request Method:</td>
<td>GET /{objectType}/{typeRelativeObjectId}/acl</td>
</tr>
<tr>
<td>Output format:</td>
<td>a JSON document of the following format<br>
<pre>{"objectType":"USER_GROUP","objectId":"82832851-07ac-47ee-9ddf-6f4f9eaa7823","displayName": null,"acl":[{"groupId":null,"actions":["READ","!DELETE"]},{"groupId":"82832851-07ac-47ee-9ddf-6f4f9eaa7823","actions":["READ","!UPDATE"]}]}
</pre>
</td>
</tr>
<tr>
<td>Parameter (path)</td>
<td>
<div>objectType</div> Object type represents the type of object to request the ACL information for
<div>objectId</div> Unique Object id.
</td>
</tr>
<tr>
<td>Example:</td>
<td><code>curl -i -X GET "http://admin:admin@127.0.0.1:8888/security/api/restsecurity/ownership/USER_GROUP/82832851-07ac-47ee-9ddf-6f4f9eaa7823/acl"</code><br>
Will provide a JSON document as explained above.</td>
</tr>
<tr>
<td>Request Method:</td>
<td>PUT /{objectType}/{typeRelativeObjectId}/acl</td>
</tr>
<tr>
<td>Output format:</td>
<td>a JSON document of the following format<br>
<pre>{ "responseStatus": "true", "responseMessage": "Ownership changed successfully"}</pre>
</td>
</tr>
<tr>
<td>Parameter (path, body)</td>
<td>
<div>objectType</div> Object type represents the type of object to request the ACL information for
<div>objectId</div> Unique Object id.
<div>The body must contain a document as the one returned by the GET method, only that the objectType and objectId fields are not required in
the body and will be ignored as these parameters are expected to be provided in the path.</div>
</td>
</tr>
<tr>
<td>Example:</td>
<td><code>curl -i -X PUT -H 'Content-Type: application/json' -d '{"objectType":"USER_GROUP","objectId":"82832851-07ac-47ee-9ddf-6f4f9eaa7823","displayName":"The Admin Tenant ACL","acl":[{"groupId":null,"actions":["READ","!UPDATE"]},{"groupId":"82832851-07ac-47ee-9ddf-6f4f9eaa7823","actions":["READ","!DELETE"]}]}' "http://admin:admin@127.0.0.1:8888/security/api/restsecurity/ownership/USER_GROUP/82832851-07ac-47ee-9ddf-6f4f9eaa7823/acl"</code><br>
Will provide a JSON document as explained above.</td>
</tr>
</table>
</body>
</html>
@@ -113,7 +113,7 @@
</tr>
</table>
<br/>
<b>Delete a user from a UserGroup</b>
<b>Delete a user from a UserGroup</b>
<table>
<tr>
<td>Request Method:</td>
@@ -151,12 +151,12 @@
<td>
<div>groupId (Path)</div> Unique name of the UserGroup to add the role to.
<div>roleId (Path)</div> Unique ID of the role to add to the UserGroup.
<div>roleAssociated (JSON Body)</div> True or false, depending whether the role's permissions are applied regardless of the user performing the access, only requiring the object access to be owned by this group; otherwise, the role's permissions are only granted if the object accessed is owned by this group and the user performing the access belongs to this group.
<div>forAll (JSON Body)</div> True or false, depending whether the role's permissions are applied regardless of the user performing the access, only requiring the object access to be owned by this group; otherwise, the role's permissions are only granted if the object accessed is owned by this group and the user performing the access belongs to this group.
</td>
</tr>
<tr>
<td>Example:</td>
<td><code>curl -X PUT -d '{"roleAssociated": true}' "127.0.0.1:8888/security/api/restsecurity/usergroup/9201d7cb-969e-4975-8cf1-32972bb2c8ba/role/29914406-b0fc-43cd-acc7-62a83931f587"</code><br></td>
<td><code>curl -X PUT -d '{"forAll": true}' "127.0.0.1:8888/security/api/restsecurity/usergroup/9201d7cb-969e-4975-8cf1-32972bb2c8ba/role/29914406-b0fc-43cd-acc7-62a83931f587"</code><br></td>
</tr>
</table>
<br/>