go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/services/frontend/static/js/api.js (about)

     1  // Copyright 2021 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  var api = (function() {
    16    'use strict';
    17  
    18    var exports = {};
    19  
    20    //// pRPC support.
    21  
    22    // A table of gRPC codes from google/rpc/code.proto.
    23    const GRPC_CODES = {
    24      OK: 0,
    25      CANCELLED: 1,
    26      UNKNOWN: 2,
    27      INVALID_ARGUMENT: 3,
    28      DEADLINE_EXCEEDED: 4,
    29      NOT_FOUND: 5,
    30      ALREADY_EXISTS: 6,
    31      PERMISSION_DENIED: 7,
    32      RESOURCE_EXHAUSTED: 8,
    33      FAILED_PRECONDITION: 9,
    34      ABORTED: 10,
    35      OUT_OF_RANGE: 11,
    36      UNIMPLEMENTED: 12,
    37      INTERNAL: 13,
    38      UNAVAILABLE: 14,
    39      DATA_LOSS: 15,
    40      UNAUTHENTICATED: 16,
    41    };
    42    exports.GRPC_CODES = GRPC_CODES;
    43  
    44    function codeToStr(code) {
    45      for (const [key, value] of Object.entries(GRPC_CODES)) {
    46        if (code == value) {
    47          return key;
    48        }
    49      }
    50      return 'CODE_'+code;
    51    };
    52  
    53    // CallError is thrown by `call` on unsuccessful responses.
    54    //
    55    // It contains the gRPC code and the error message.
    56    class CallError extends Error {
    57      constructor(code, error) {
    58        super(`pRPC call failed with code ${codeToStr(code)}: ${error}`);
    59        this.name = 'CallError';
    60        this.code = code;
    61        this.error = error;
    62      }
    63    };
    64    exports.CallError = CallError;
    65  
    66    // Calls a pRPC method.
    67    //
    68    // Args:
    69    //   service: the full service name e.g. "auth.service.Accounts".
    70    //   method: a pRPC method name e.g. "GetSelf".
    71    //   request: a dict with JSON request body.
    72    //
    73    // Returns:
    74    //   The response as a JSON dict.
    75    //
    76    // Throws:
    77    //   CallError (both on network issues and non-OK responses).
    78    async function call(service, method, request) {
    79      try {
    80        // See https://pkg.go.dev/go.chromium.org/luci/grpc/prpc for definition of
    81        // the pRPC protocol (in particular its JSON encoding).
    82        let resp = await fetch('/prpc/' + service + '/' + method, {
    83          method: 'POST',
    84          headers: {
    85            'Accept': 'application/prpc; encoding=json',
    86            'Content-Type': 'application/prpc; encoding=json',
    87            'X-Xsrf-Token': xsrf_token,
    88          },
    89          credentials: 'same-origin',
    90          cache: 'no-cache',
    91          redirect: 'error',
    92          referrerPolicy: 'no-referrer',
    93          body: JSON.stringify(request || {}),
    94        });
    95        let body = await resp.text();
    96  
    97        // Valid pRPC responses must have 'X-Prpc-Grpc-Code' header with an
    98        // integer code. The only exception is >=500 HTTP status replies. They are
    99        // internal errors that can be thrown even before the pRPC server is
   100        // reached.
   101        let code = parseInt(resp.headers.get('X-Prpc-Grpc-Code'));
   102        if (code == NaN) {
   103          if (resp.status >= 500) {
   104            throw new CallError(GRPC_CODES.INTERNAL, body);
   105          }
   106          throw new CallError(
   107            GRPC_CODES.INTERNAL,
   108            'no valid X-Prpc-Grpc-Code in the response',
   109          );
   110        }
   111  
   112        // Non-OK responses have the error message as their body.
   113        if (code != GRPC_CODES.OK) {
   114          throw new CallError(code, body);
   115        }
   116  
   117        // OK responses start with `)]}'\n`, followed by the JSON response body.
   118        const prefix = ')]}\'\n';
   119        if (!body.startsWith(prefix)) {
   120          throw new CallError(
   121            GRPC_CODES.INTERNAL,
   122            'missing the expect JSON response prefix',
   123          );
   124        }
   125  
   126        return JSON.parse(body.slice(prefix.length));
   127      } catch (err) {
   128        if (err instanceof CallError) {
   129          throw err;
   130        } else {
   131          throw new CallError(GRPC_CODES.INTERNAL, err.message);
   132        }
   133      }
   134    };
   135    exports.call = call;
   136  
   137  
   138    //// API calls.
   139  
   140    // Get all groups.
   141    exports.groups = function() {
   142      return call('auth.service.Groups', 'ListGroups');
   143    };
   144  
   145    // Get individual group.
   146    exports.groupRead = function(request) {
   147      return call('auth.service.Groups', 'GetGroup', {'name': request});
   148    };
   149  
   150    // Delete individual group.
   151    exports.groupDelete = function(name, etag) {
   152      return call('auth.service.Groups', 'DeleteGroup', {'name': name, 'etag': etag});
   153    };
   154  
   155    // Create a group.
   156    exports.groupCreate = function(authGroup) {
   157      return call('auth.service.Groups', 'CreateGroup', {'group': authGroup});
   158    }
   159  
   160    // Update a group.
   161    exports.groupUpdate = function(authGroup) {
   162      return call('auth.service.Groups', 'UpdateGroup', {'group': authGroup});
   163    }
   164  
   165    // Get all allowlists.
   166    exports.ipAllowlists = function() {
   167      return call('auth.service.Allowlists', 'ListAllowlists');
   168    };
   169  
   170    // Get all changeLogs.
   171    exports.changeLogs = function(target, revision, pageSize, pageToken) {
   172      var q = {};
   173      if (target) {
   174        q['target'] = target;
   175      }
   176      if (revision) {
   177        q['auth_db_rev'] = revision;
   178      }
   179      if (pageSize) {
   180        q['page_size'] = pageSize;
   181      }
   182      if (pageToken) {
   183        q['page_token'] = pageToken;
   184      }
   185  
   186      return call('auth.service.ChangeLogs', 'ListChangeLogs', q)
   187    }
   188  
   189    //// XSRF token utilities.
   190  
   191  
   192    // The current known value of the XSRF token.
   193    var xsrf_token = null;
   194  
   195    // Sets the XSRF token.
   196    exports.setXSRFToken = function(token) {
   197      xsrf_token = token;
   198    };
   199  
   200    // Enables the XSRF token refresh timer (firing once an hour).
   201    exports.startXSRFTokenAutoupdate = function() {
   202      setInterval(() => {
   203        call('auth.internals.Internals', 'RefreshXSRFToken', {
   204          'xsrfToken': xsrf_token,
   205        }).then(resp => {
   206          xsrf_token = resp.xsrfToken;
   207        });
   208      }, 3600*1000);
   209    };
   210  
   211    return exports;
   212  }());