summaryrefslogtreecommitdiff
path: root/adservices/service-core/java/com/android/adservices/service/shell/AdServicesShellCommandHandler.java
blob: 08efcc33d79da6b105d21c0921a20d87ac62bb0f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.adservices.service.shell;

import android.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;

import com.android.adservices.service.common.AppManifestConfigHelper;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;

import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Objects;

// TODO(b/308009734): STOPSHIP - document that it's up to each command implementation to check about
// caller's permission, whether device is userdebug, etc...

// TODO(b/308009734): arg parsing logic mostly copied from on BasicShellCommandHandler, it might be
// worth to refactor it once the handler is called by both system_server and service.

/**
 * Service-side version of {@code cmd adservices_manager}.
 *
 * <p>By convention, methods implementing commands should be prefixed with {@code run}.
 */
public final class AdServicesShellCommandHandler {

    @VisibleForTesting static final String CMD_SHORT_HELP = "-h";
    @VisibleForTesting static final String CMD_HELP = "help";
    @VisibleForTesting static final String CMD_ECHO = "echo";

    @VisibleForTesting
    static final String CMD_IS_ALLOWED_ATTRIBUTION_ACCESS = "is-allowed-attribution-access";

    @VisibleForTesting
    static final String CMD_IS_ALLOWED_CUSTOM_AUDIENCES_ACCESS =
            "is-allowed-custom-audiences-access";

    @VisibleForTesting
    static final String CMD_IS_ALLOWED_TOPICS_ACCESS = "is-allowed-topics-access";

    @VisibleForTesting
    static final String HELP_ECHO =
            CMD_ECHO + " <message> - prints the given message (useful to check cmd is working).";

    @VisibleForTesting
    static final String HELP_IS_ALLOWED_ATTRIBUTION_ACCESS =
            CMD_IS_ALLOWED_ATTRIBUTION_ACCESS
                    + " <package_name> <enrollment_id> - checks if the given enrollment id is"
                    + " allowed to use the Attribution APIs in the given app.";

    @VisibleForTesting
    static final String HELP_IS_ALLOWED_CUSTOM_AUDIENCES_ACCESS =
            CMD_IS_ALLOWED_CUSTOM_AUDIENCES_ACCESS
                    + " <package_name> <enrollment_id> - checks if the given enrollment id is"
                    + " allowed to use the Custom Audience APIs in the given app.";

    @VisibleForTesting
    static final String HELP_IS_ALLOWED_TOPICS_ACCESS =
            CMD_IS_ALLOWED_TOPICS_ACCESS
                    + " <package_name> <enrollment_id> <using_sdk_sandbox>- checks if the given"
                    + " enrollment id is allowed to use the Topics APIs in the given app, when"
                    + " using SDK sandbox or not.";

    @VisibleForTesting static final String ERROR_EMPTY_COMMAND = "Must provide a non-empty command";

    @VisibleForTesting
    static final String ERROR_TEMPLATE_INVALID_ARGS = "Invalid cmd (%s). Syntax: %s";

    // TODO(b/280460130): use adservice helpers for tag name / logging methods
    private static final String TAG = "AdServicesShellCmd";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    private static final int RESULT_OK = 0;
    private static final int RESULT_GENERIC_ERROR = -1;

    private final PrintWriter mOut;

    private String[] mArgs;
    private int mArgPos;
    private String mCurArgData;

    public AdServicesShellCommandHandler(PrintWriter out) {
        mOut = Objects.requireNonNull(out, "out cannot be null");
    }

    /** Runs the given command ({@code args[0]}) and optional arguments */
    public int run(String... args) {
        Objects.requireNonNull(args, "args cannot be null");
        Preconditions.checkArgument(
                args.length >= 1, "must have at least one argument (the command itself)");
        if (DEBUG) {
            Log.d(TAG, "run(): " + Arrays.toString(args));
        }
        mArgs = args;
        String cmd = getNextArg();

        int res = RESULT_GENERIC_ERROR;
        try {
            res = onCommand(cmd);
            if (DEBUG) {
                Log.d(TAG, "Executed command " + cmd);
            }
        } catch (Throwable e) {
            // TODO(b/308009734): need to test this
            mOut.printf("Exception occurred while executing %s\n", Arrays.toString(mArgs));
            e.printStackTrace(mOut);
        } finally {
            if (DEBUG) {
                Log.d(TAG, "Flushing output");
            }
            mOut.flush();
        }
        if (DEBUG) {
            Log.d(TAG, "Sending command result: " + res);
        }
        return res;
    }

    /******************
     * Helper methods *
     ******************/

    @Nullable
    private String getNextArg() {
        if (mArgs == null) {
            throw new IllegalStateException("INTERNAL ERROR: getNextArg() called before run()");
        }
        if (mCurArgData != null) {
            String arg = mCurArgData;
            mCurArgData = null;
            return arg;
        } else if (mArgPos < mArgs.length) {
            return mArgs[mArgPos++];
        } else {
            return null;
        }
    }

    private boolean hasExactNumberOfArgs(int expected) {
        return mArgs.length == expected + 1; // adds +1 for the cmd itself
    }

    @Nullable
    private Boolean getNextBooleanArg() {
        String arg = getNextArg();
        if (TextUtils.isEmpty(arg)) {
            return null;
        }
        // Boolean.parse returns false when it's invalid
        switch (arg.trim().toLowerCase()) {
            case "true":
                return Boolean.TRUE;
            case "false":
                return Boolean.FALSE;
            default:
                return null;
        }
    }

    private int invalidArgsError(String syntax) {
        mOut.println(String.format(ERROR_TEMPLATE_INVALID_ARGS, Arrays.toString(mArgs), syntax));
        return RESULT_GENERIC_ERROR;
    }

    /****************************************************************************
     * Commands - for each new one, add onHelp(), onCommand(), and runCommand() *
     ****************************************************************************/

    private void onHelp() {
        mOut.println(HELP_ECHO);
        mOut.println(HELP_IS_ALLOWED_ATTRIBUTION_ACCESS);
        mOut.println(HELP_IS_ALLOWED_CUSTOM_AUDIENCES_ACCESS);
        mOut.println(HELP_IS_ALLOWED_TOPICS_ACCESS);
    }

    private int onCommand(String cmd) {
        switch (cmd) {
            case CMD_SHORT_HELP:
            case CMD_HELP:
                onHelp();
                return RESULT_GENERIC_ERROR;
            case "":
                mOut.println(ERROR_EMPTY_COMMAND);
                return RESULT_GENERIC_ERROR;
            case CMD_ECHO:
                return runEcho();
            case CMD_IS_ALLOWED_ATTRIBUTION_ACCESS:
            case CMD_IS_ALLOWED_CUSTOM_AUDIENCES_ACCESS:
            case CMD_IS_ALLOWED_TOPICS_ACCESS:
                return runIsAllowedApiAccess(cmd);
            default:
                mOut.printf("Unknown command: %s\n", cmd);
                return RESULT_GENERIC_ERROR;
        }
    }

    private int runEcho() {
        if (!hasExactNumberOfArgs(1)) {
            return invalidArgsError(HELP_ECHO);
        }
        String message = getNextArg();
        if (TextUtils.isEmpty(message)) {
            return invalidArgsError(HELP_ECHO);
        }

        Log.i(TAG, "runEcho: message='" + message + "'");
        mOut.println(message);
        return RESULT_OK;
    }

    private int runIsAllowedApiAccess(String cmd) {
        int expectedArgs = 2; // first 2 args are common for all of them
        String helpMsg = null;
        switch (cmd) {
            case CMD_IS_ALLOWED_ATTRIBUTION_ACCESS:
                helpMsg = HELP_IS_ALLOWED_ATTRIBUTION_ACCESS;
                break;
            case CMD_IS_ALLOWED_CUSTOM_AUDIENCES_ACCESS:
                helpMsg = HELP_IS_ALLOWED_CUSTOM_AUDIENCES_ACCESS;
                break;
            case CMD_IS_ALLOWED_TOPICS_ACCESS:
                expectedArgs = 3;
                helpMsg = HELP_IS_ALLOWED_TOPICS_ACCESS;
                break;
        }
        if (!hasExactNumberOfArgs(expectedArgs)) {
            return invalidArgsError(helpMsg);
        }
        String pkgName = getNextArg();
        if (TextUtils.isEmpty(pkgName)) {
            return invalidArgsError(helpMsg);
        }
        String enrollmentId = getNextArg();
        if (TextUtils.isEmpty(enrollmentId)) {
            return invalidArgsError(helpMsg);
        }

        boolean isValid = false;
        switch (cmd) {
            case CMD_IS_ALLOWED_ATTRIBUTION_ACCESS:
                isValid = AppManifestConfigHelper.isAllowedAttributionAccess(pkgName, enrollmentId);
                Log.i(
                        TAG,
                        "isAllowedAttributionAccess("
                                + pkgName
                                + ", "
                                + enrollmentId
                                + ": "
                                + isValid);
                break;
            case CMD_IS_ALLOWED_CUSTOM_AUDIENCES_ACCESS:
                isValid =
                        AppManifestConfigHelper.isAllowedCustomAudiencesAccess(
                                pkgName, enrollmentId);
                Log.i(
                        TAG,
                        "isAllowedCustomAudiencesAccess("
                                + pkgName
                                + ", "
                                + enrollmentId
                                + ": "
                                + isValid);
                break;
            case CMD_IS_ALLOWED_TOPICS_ACCESS:
                Boolean usesSdkSandbox = getNextBooleanArg();
                if (usesSdkSandbox == null) {
                    return invalidArgsError(HELP_IS_ALLOWED_TOPICS_ACCESS);
                }
                isValid =
                        AppManifestConfigHelper.isAllowedTopicsAccess(
                                usesSdkSandbox, pkgName, enrollmentId);
                Log.i(
                        TAG,
                        "isAllowedTopicAccess("
                                + pkgName
                                + ", "
                                + usesSdkSandbox
                                + ", "
                                + enrollmentId
                                + ": "
                                + isValid);
                break;
        }
        mOut.println(isValid);
        return RESULT_OK;
    }
}