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 }());