-
Notifications
You must be signed in to change notification settings - Fork 31
[AGDROID-771] Batch security metrics and update json schema #126
[AGDROID-771] Batch security metrics and update json schema #126
Conversation
unitTests.includeAndroidResources = true | ||
unitTests { | ||
includeAndroidResources = true | ||
returnDefaultValues = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this because of http://tools.android.com/tech-docs/unit-testing-support#TOC-Method-...-not-mocked.- errors.
Not sure if it's okay but seemed easier than mocking and DI-ing JSONObject
and Looper
.
final ArrayList<SecurityCheckResult> results = new ArrayList<>(howMany); | ||
for (int i = 0; i < howMany; i++) { | ||
try { | ||
SecurityCheckResult result = cs.take().get(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be the recommended way to consume these but I'm not sure if it's very solid.
@secondsun @danielpassos Is there a risk these could get mixed up with other tasks in the same ExecutorService
?
It seems that by default it's the AppExecutors#singleThreadService()
singleton and this method is set to be run in the networkThread()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by "mixed up"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@secondsun maybe the race condition is only in my head.
IIUC the executorService is a queue so I thought that maybe if somewhere else in the main thread other kinds of jobs are queued into the singleThreadService
the cs.take()
call could end up picking a result that's meant for another async operation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh right. So 1) The network thread is single threaded. That isn't guaranteed, but it is right now. 2) If the order is important there are options for enforcing the order of the jobs, but I will have to refresh myself. (Or we add a epic for weaving RxJava into the SDK implementation)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@paolobueno With the suggested approach, this problem does not exists (see my comment)
public interface Metrics { | ||
|
||
String identifier(); | ||
|
||
Map<String, String> data(); | ||
Object data(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had to do this to support JSONArray
. JSONArray
and JSONObject
don't share any ancestors.
The goal is to be able to have an entry with the shape:
{
{{identifier}}: [data]
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both implementations we provide use JSONObject, so why not a JSONObject? In the future if we need this then we can raise the interface's type without breaking compatibility.
Alternatively we can define the the Metrics data as a generic type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to reach the following shape from aerogear-attic/aerogear-app-metrics#33:
"security": [
{
"type": "org.aerogear.mobile.security.checks.DeveloperModeCheck",
"passed": true
},
{
"type": "org.aerogear.mobile.security.checks.EmulatorCheck",
"passed": false
},
{
"type": "org.aerogear.mobile.security.checks.DebuggerCheck",
"passed": false
},
{
"type": "org.aerogear.mobile.security.checks.RootedCheck",
"passed": false
},
{
"type": "org.aerogear.mobile.security.checks.ScreenLockCheck",
"passed": false
}
]
So I changed SecurityCheckResultMetric
to be {identifier: "security", data: JSONArray()}
.
I guess we could somehow change NetworkMetricsPublisher#publish
instead.
Do you see any benefit of having Metrics<T>
without a type constraint over Object
?
I didn't see much point doing that because even https://developer.android.com/reference/org/json/JSONObject.html#put(java.lang.String,java.lang.Object) receives Object
over having overloads for JSONObject|Array
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The best thing that a generic type parameter gets us is a little bit of developer ease at code writing time. Depending on how much people will put in their Metrics implementations it may or may not be beneficial.
What the generic type does is make explicit the data type that is being wrapped by the metrics class. Functionality wise it won't behave differently than the current proposal, but it does make the down typing you can do on the data method more explicit.
71e966e
to
548d938
Compare
I'm interested in seeing how this works. Mind tossing in some steps for how to test it using openshift and the mobile features? |
@secondsun thanks for the review, I only paid attention to the unit tests so far, going to sync up with @darahayes because verification would require aerogear-attic/aerogear-app-metrics#33 too. I guess it'd actually be the goal of https://issues.jboss.org/browse/AEROGEAR-2218 too. |
@paolobueno can you please revisit the server side PR here: aerogear-attic/aerogear-app-metrics#33 The server now expects the security checks to have a The format should now look like this:
|
@@ -40,7 +40,8 @@ | |||
private OIDCAuthenticatorImpl oidcAuthenticatorImpl; | |||
|
|||
private Context appContext; | |||
private Logger logger; | |||
private final Logger LOG = MobileCore.getLogger(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this static
if (metricsService != null) { | ||
metricsService.publish(new SecurityCheckResultMetric(result)); | ||
} | ||
final Collection<SecurityCheck> checks = getChecks(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be easier like this:
public Map<String, Future<SecurityCheckResult>> execute() {
final MetricsService metricsService = getMetricsService();
final Map<String, Future<SecurityCheckResult>> res = new HashMap<>();
final List<SecurityCheckResultMetric> metrics = new ArrayList<>();
final Collection<SecurityCheck> checks = getChecks();
for (final SecurityCheck check : checks) {
res.put(check.getName(), (executorService.submit(() -> {
final SecurityCheckResult result = check.test(getContext());
if (metricsService != null) {
synchronized (metrics) {
metrics.add(new SecurityCheckResultMetric(result));
if (metrics.size() == checks.size()) {
metricsService.publish(metrics.toArray(new SecurityCheckResultMetric[metrics.size()]));
}
}
}
return result;
})));
}
return res;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, wasn't familiar with synchronized methods and blocks.
I was, however, thinking about ways that users could do this kind of queuing of actions themselves. Both of these approaches are only possible because we're in control of the Callables or aware of the ExecutorService.
I guess we can postpone these improvements for Rx 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@paolobueno TBH I think we shouldn't change the executor to batch the metrics, but batching should be done by the metric service (see my comment at the end of the PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite understand why AsyncSecurityCheckExecutor is handled like that. Wouldn't it be possible to do something like wrapping SyncSecurityCheckExecutor
in a Future?
@@ -40,7 +40,8 @@ | |||
private OIDCAuthenticatorImpl oidcAuthenticatorImpl; | |||
|
|||
private Context appContext; | |||
private Logger logger; | |||
private final Logger LOG = MobileCore.getLogger(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LOGGER
would be more proper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest private finals are declared before standard fields and after public finals.
@@ -62,7 +61,7 @@ public String getRefreshToken() { | |||
|
|||
/** | |||
* Verify the token and its claims against the given Keycloak configuration | |||
* | |||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is why we need to fix the linter/format issues once for all
final int checkCount = checks.size(); | ||
for (final SecurityCheck check : checks) { | ||
final Future<SecurityCheckResult> future = executorService.submit(() -> { | ||
final SecurityCheckResult result = check.test(getContext()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be in one line:
return check.test(getContext());
or even:
executorService.submit(() -> check.test(getContext()));
metricsService.publish(new SecurityCheckResultMetric(result)); | ||
} | ||
final Collection<SecurityCheck> checks = getChecks(); | ||
final int checkCount = checks.size(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this constant is needed. In any case I would declare it after the loop, for readability.
res.put(check.getName(), future); | ||
} | ||
|
||
if(metricsService != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is missing a space if (..
We definitely need to fix the linter/format issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the constant metricsService
is unnecessary or in any case I would declare it right before using it. They it is, it looks like a field. Moreover,publishMetricsAsync
is using the getter directly.
|
||
if (metricsService != null) { | ||
metricsService.publish(new SecurityCheckResultMetric(nonNull(result, "result"))); | ||
private void publishResultMetrics(final @NonNull Map<String, SecurityCheckResult> results) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a small detail but publishResultMetrics
shouldn't check if metricsService is null. That should be checked before calling this method. Otherwise the method should be called something like publishResultMetricsIfPossible
. In AsyncSecurityCheckExecutor
it is checked this way for instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
private final Logger LOG = MobileCore.getLogger(); | ||
private final String TAG = "SecurityCheckResultMetric"; | ||
|
||
public static final String IDENTIFIER = "security"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest declaring public static finals on top of anything, for better visibility.
public SecurityCheckResultMetric(@NonNull final SecurityCheckResult result) { | ||
this.identifier = nonNull(result, "result").getName(); | ||
this.data = getDataFromResult(result); | ||
public SecurityCheckResultMetric(@NonNull final SecurityCheckResult... results) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this constructor? It's used only in a test.
@@ -48,19 +66,32 @@ public String identifier() { | |||
* the value is <code>true</code> if the check result passed | |||
*/ | |||
@Override | |||
public Map<String, String> data() { | |||
return Collections.unmodifiableMap(data); | |||
public JSONArray data() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please update doc comments.
public Map<String, String> data() { | ||
return Collections.unmodifiableMap(data); | ||
public JSONArray data() { | ||
// TODO: consider returning a deep clone |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not create the JSONArray during instantiation but rather store the results as class field and return a new JSONArray each time:
public SecurityCheckResultMetric(@NonNull final Iterable<SecurityCheckResult> results) {
this.results = results;
}
public SecurityCheckResultMetric(@NonNull final SecurityCheckResult... results) {
this.results = Arrays.asList(results);
}
public JSONArray data() {
return getDataFromResult(this.results);
}
Also I'd add a test to ensure data cannot be changed if this would be a problem.
@josemigallas as you can see here, the
While, on the other hand, the code to be written would be almost the same. |
} | ||
|
||
return res; | ||
} | ||
|
||
private void publishMetricsAsync(final int howMany) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the suggested approach, this method is useless
I think having a single sync Executor and running it in a background thread could be enough. Paralellizing all the self-defence checks seems like a premature optimization, though the code is mostly in place. Do you guys think there'll be any long-running self-defence check that would warrant being run in parallel? Wasn't planning on doing this change as part of this PR though. |
@paolobueno Not sure I get it. The I thought this PR was just about batching the metrics: even about that, I think we are making the code unnecessarily complex. SecurityCheckExecutor.newAsyncCheckExecutor(context)
...
...
.withMetricsPublisher(new MetricPublisher(10))
.build().execute(); Then the publisher could (in a background thread):
@secondsun @wei-lee : WDYT? |
Making the publisher smarter was mentioned in https://issues.jboss.org/browse/AGDROID-771?focusedCommentId=13540352&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-13540352 too. Agree it'd be nice to have but I'd like to leave this out of the scope of this PR if possible. |
|
||
int checkCount = getChecks().size(); | ||
if (results == null) { | ||
results = new ArrayList<>(checkCount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This way if the called will call 2 times the execute method, metrics will be messed up and some metric will be lost.
With the proposed approach, we would not have this issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the method being synchronized
would prevent this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. The result array is a member variable. If I call the execute two times, I will add the results twice in the same array
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see what you mean, the executor interface doesn't prevent it being executed multiple times. 👍
@paolobueno If I do execute from Here is an example what I did: securityService.getCheckExecutor()
.addCheck(SecurityCheckType.ALLOW_BACKUP_ENABLED)
.addCheck(SecurityCheckType.IS_EMULATOR)
.addCheck(SecurityCheckType.IS_DEBUGGER)
.addCheck(SecurityCheckType.IS_DEVELOPER_MODE)
.addCheck(SecurityCheckType.IS_ROOTED)
.addCheck(SecurityCheckType.SCREEN_LOCK_ENABLED)
.addCheck(SecurityCheckType.HAS_ENCRYPTION_ENABLED)
.execute(); |
Added event listener
} | ||
|
||
return res; | ||
} | ||
|
||
private synchronized void tallyResultsAndPublish(SecurityCheckResult result, List<SecurityCheckResult> results) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@paolobueno This way it would work, however synchronising the whole method, you synchronize parts of the code that doesn't needs to be synchronized (thus slowing the execution).
I have created a PR to propose a different implementation for the batching. Would you mind giving that a look?
#129
@danielpassos That's true. IMHO I think that the To do what you want we should add a However That does not happen if we use directly the builders since they were designed to produce immutable objects. Using the builders, your code would be: SecurityCheckExecutor
.newSyncExecutor(getContext()) // or newAsyncExecutor(getContext()) if you need an async
.withSecurityCheck(SecurityCheckType.ALLOW_BACKUP_ENABLED)
.withSecurityCheck(SecurityCheckType.IS_EMULATOR)
.withSecurityCheck(SecurityCheckType.IS_DEBUGGER)
.withSecurityCheck(SecurityCheckType.IS_DEVELOPER_MODE)
.withSecurityCheck(SecurityCheckType.IS_ROOTED)
.withSecurityCheck(SecurityCheckType.SCREEN_LOCK_ENABLED)
.withSecurityCheck(SecurityCheckType.HAS_ENCRYPTION_ENABLED)
.withMetricService(metricService)
.build()
.execute(); |
@paolobueno can you take a look at PR #129. I think it is a much simpler and cleaner way to implement this function. |
…from the end user
7e84d68
to
ed523c6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor comments, then LGTM
/** | ||
* Called when all submitted checks has been executed. | ||
*/ | ||
void onFinished(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as suggested by @matzew in the other PR, rename this to onComplete
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
Motivation
JIRA: https://issues.jboss.org/browse/AGDROID-771
Description
Make both Sync and Async executors send a single metrics request upon finishing execution.
Update data being sent to match the server-side implementation at aerogear-attic/aerogear-app-metrics#33
Progress
type
fieldsyncroninzed
keyword