├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── .vscode └── launch.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── dist ├── index.d.ts ├── index.js └── index.js.map ├── draw.xml ├── examples └── deployment.yaml ├── graph.png ├── index.ts ├── package-lock.json ├── package.json ├── test ├── index.ts └── mocha.opts ├── tsconfig.json └── tslint.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # google credentials 61 | .credentials.json -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module", 9 | "ecmaVersion": 2017 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 4 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "double" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ] 28 | } 29 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # google credentials 61 | .credentials.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "runtimeArgs": [ 12 | "-r", 13 | "ts-node/register" 14 | ], 15 | "args": [ 16 | "${workspaceFolder}/index.ts", 17 | "--rules=team1@example.com:org1:Admin", 18 | "--rules=team1@example.com:org2:Admin", 19 | "--rules=team2@example.com:org1:Admin", 20 | "--mode=sync", 21 | "--exclude-role=Admin", 22 | "--grafana-protocol=http", 23 | "--grafana-host=127.0.0.1:3000", 24 | "--grafana-username=rewe", 25 | "--google-admin-email=admin@example.com", 26 | "--google-credentials=${workspaceFolder}/credentials.json", 27 | "--level=info" 28 | ] 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.0.1 2 | 3 | - Fix static rule sync error 4 | 5 | ## v1.0.0 6 | 7 | - First version -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | WORKDIR /app 3 | ENV NODE_ENV=production 4 | COPY . /app 5 | RUN set -e; yarn install; 6 | ENTRYPOINT [ "yarn", "start" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Roman Rogozhnikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grafana-gsuite-sync (deprecated) 2 | [![license](https://img.shields.io/github/license/google-cloud-tools/grafana-gsuite-sync.svg?maxAge=604800)](https://github.com/google-cloud-tools/grafana-gsuite-sync) 3 | [![Docker Repository on Quay](https://quay.io/repository/google-cloud-tools/grafana-gsuite-sync/status "Docker Repository on Quay")](https://quay.io/repository/google-cloud-tools/grafana-gsuite-sync) 4 | 5 | ## >> This tool has been deprecated, use [grafana-permission-sync](https://github.com/cloudworkz/grafana-permission-sync) instead 6 | 7 | ### What It Does 8 | 9 | Grafana GSuite Synchroniser pulls a Google Group, extracts Google Group Member Emails and updates the Grafana Organisation Users. 10 | 11 | [![graph](https://raw.githubusercontent.com/google-cloud-tools/grafana-gsuite-sync/master/graph.png)](https://raw.githubusercontent.com/google-cloud-tools/grafana-gsuite-sync/master/graph.png) 12 | 13 | ### Requirements 14 | 15 | - The service account's private key file: **--google-credentials** flag 16 | - The email of the user with permissions to access the Admin APIs: **--google-admin-email** flag 17 | - The grafana admin password: **--grafana-password** flag 18 | 19 | ### Usage 20 | 21 | ``` 22 | docker run -it quay.io/google-cloud-tools/grafana-gsuite-sync -h 23 | 24 | Usage: grafana-gsuite-sync [options] 25 | 26 | Options: 27 | 28 | -p, --port [port] Server port 29 | -P, --grafana-protocol [grafana-protocol] Grafana API protocol 30 | -H, --grafana-host [grafana-host] Grafana API host 31 | -U, --grafana-username [grafana-username] Grafana API admin username (default: ) 32 | -P, --grafana-password Grafana API admin password (default: ) 33 | -C, --google-credentials Path to google admin directory credentials file (default: ) 34 | -A, --google-admin-email The Google Admin Email for subject (default: ) 35 | -r, --rules Comma separated rules to sync :: 36 | (e.g. 'group@test.com:Main:Admin') 37 | -s, --static-rules Comma separated static rules to create :: 38 | (e.g. 'user@test.com:Main:Viewer') 39 | -l, --level [level] Log level 40 | -m, --mode [mode] How users are sychronized between google and grafana: sync or upsert-only 41 | -e, --exclude-role [exclude-role] Exclude role to delete 42 | -i, --interval [interval] Sync interval 43 | -h, --help output usage information 44 | ``` 45 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | export {}; 3 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 4 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 5 | return new (P || (P = Promise))(function (resolve, reject) { 6 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 7 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 8 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 9 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 10 | }); 11 | }; 12 | var __generator = (this && this.__generator) || function (thisArg, body) { 13 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 14 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 15 | function verb(n) { return function (v) { return step([n, v]); }; } 16 | function step(op) { 17 | if (f) throw new TypeError("Generator is already executing."); 18 | while (_) try { 19 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 20 | if (y = 0, t) op = [op[0] & 2, t.value]; 21 | switch (op[0]) { 22 | case 0: case 1: t = op; break; 23 | case 4: _.label++; return { value: op[1], done: false }; 24 | case 5: _.label++; y = op[1]; op = [0]; continue; 25 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 26 | default: 27 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 28 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 29 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 30 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 31 | if (t[2]) _.ops.pop(); 32 | _.trys.pop(); continue; 33 | } 34 | op = body.call(thisArg, _); 35 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 36 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 37 | } 38 | }; 39 | Object.defineProperty(exports, "__esModule", { value: true }); 40 | var commander = require("commander"); 41 | var express = require("express"); 42 | var fs_1 = require("fs"); 43 | var google_auth_library_1 = require("google-auth-library"); 44 | var googleapis_1 = require("googleapis"); 45 | var pino = require("pino"); 46 | var prom_client_1 = require("prom-client"); 47 | var request = require("request-promise"); 48 | var util_1 = require("util"); 49 | var readFileAsync = util_1.promisify(fs_1.readFile); 50 | var collect = function (value, previous) { 51 | var splitValue = Array.isArray(value) ? value : value.split(","); 52 | return previous.concat(splitValue); 53 | }; 54 | commander 55 | .option("-p, --port [port]", "Server port") 56 | .option("-P, --grafana-protocol [grafana-protocol]", "Grafana API protocol", /^(http|https)$/i) 57 | .option("-H, --grafana-host [grafana-host]", "Grafana API host") 58 | .option("-U, --grafana-username [grafana-username]", "Grafana API admin username", "") 59 | .option("-P, --grafana-password ", "Grafana API admin password", "") 60 | .option("-C, --google-credentials ", "Path to google admin directory credentials file", "") 61 | .option("-A, --google-admin-email ", "The Google Admin Email for subject", "") 62 | .option("-r, --rules ", "Comma separated or repeatable rules to sync :: \n\t" + 63 | "(e.g. 'group@test.com:Main:Admin')", collect, []) 64 | .option("-s, --static-rules ", "Comma separated or repeatable static rules to create :: \n\t" + 65 | "(e.g. 'user@test.com:Main:Viewer')", collect, []) 66 | .option("-l, --level [level]", "Log level", /^(debug|info|warn|error|fatal)$/i) 67 | .option("-m, --mode [mode]", "How users are sychronized between google and grafana: sync or upsert-only", /^(sync|upsert-only)$/i) 68 | .option("-e, --exclude-role [exclude-role]", "Exclude role to delete", /^(Admin|Editor|Viewer)$/i) 69 | .option("-i, --interval [interval]", "Sync interval") 70 | .parse(process.argv); 71 | var app = express(); 72 | var port = process.env.PORT || commander.port || 5000; 73 | var GrafanaSync = (function () { 74 | function GrafanaSync() { 75 | var _this = this; 76 | this.getGoogleApiClient = function () { return __awaiter(_this, void 0, void 0, function () { 77 | var content, credentials, client, e_1; 78 | return __generator(this, function (_a) { 79 | switch (_a.label) { 80 | case 0: 81 | if (this.service && this.client) { 82 | return [2]; 83 | } 84 | _a.label = 1; 85 | case 1: 86 | _a.trys.push([1, 4, , 5]); 87 | this.logger.debug("Get google api client"); 88 | return [4, readFileAsync(this.credentialsPath)]; 89 | case 2: 90 | content = _a.sent(); 91 | credentials = JSON.parse(content.toString()); 92 | client = google_auth_library_1.auth.fromJSON(credentials, {}); 93 | client.subject = this.googleAdminEmail; 94 | client.scopes = [ 95 | "https://www.googleapis.com/auth/admin.directory.group.member.readonly", 96 | "https://www.googleapis.com/auth/admin.directory.group.readonly", 97 | ]; 98 | return [4, client.authorize()]; 99 | case 3: 100 | _a.sent(); 101 | this.client = client; 102 | this.service = googleapis_1.google.admin("directory_v1"); 103 | return [3, 5]; 104 | case 4: 105 | e_1 = _a.sent(); 106 | this.logger.error("Failed to get google api client", { error: this.formatError(e_1) }); 107 | return [3, 5]; 108 | case 5: return [2]; 109 | } 110 | }); 111 | }); }; 112 | this.getGroupMembers = function (email, nextPageToken) { 113 | if (nextPageToken === void 0) { nextPageToken = ""; } 114 | return __awaiter(_this, void 0, void 0, function () { 115 | var options, response, members_1, pageMembers, e_2; 116 | var _this = this; 117 | return __generator(this, function (_a) { 118 | switch (_a.label) { 119 | case 0: 120 | _a.trys.push([0, 6, , 7]); 121 | return [4, this.getGoogleApiClient()]; 122 | case 1: 123 | _a.sent(); 124 | if (!this.service || !this.client) { 125 | this.logger.debug("The google api is not configured."); 126 | return [2, []]; 127 | } 128 | options = { 129 | auth: this.client, 130 | groupKey: email, 131 | }; 132 | if (nextPageToken) { 133 | options.pageToken = nextPageToken; 134 | } 135 | return [4, this.service.members.list(options)]; 136 | case 2: 137 | response = _a.sent(); 138 | if (response.status !== 200 || !response.data || !response.data.members) { 139 | throw new Error("Failed to get members list."); 140 | } 141 | this.logger.debug("Got google response members", { members: response.data.members.map(function (m) { return m.email; }) }); 142 | members_1 = []; 143 | return [4, Promise.all(response.data.members.filter(function (m) { return m.email; }).map(function (member) { return __awaiter(_this, void 0, void 0, function () { 144 | var subMembers; 145 | return __generator(this, function (_a) { 146 | switch (_a.label) { 147 | case 0: 148 | if (!(member.type === "GROUP")) return [3, 2]; 149 | return [4, this.getGroupMembers(member.email)]; 150 | case 1: 151 | subMembers = _a.sent(); 152 | members_1 = members_1.concat(subMembers); 153 | return [3, 3]; 154 | case 2: 155 | members_1.push(member.email); 156 | _a.label = 3; 157 | case 3: return [2]; 158 | } 159 | }); 160 | }); }))]; 161 | case 3: 162 | _a.sent(); 163 | if (!response.data.nextPageToken) return [3, 5]; 164 | this.logger.debug("Find next page"); 165 | return [4, this.getGroupMembers(email, response.data.nextPageToken)]; 166 | case 4: 167 | pageMembers = _a.sent(); 168 | this.logger.debug("Got google page members", { members: pageMembers }); 169 | members_1 = members_1.concat(pageMembers); 170 | _a.label = 5; 171 | case 5: 172 | this.logger.debug({ members: members_1 }, "Got google members"); 173 | return [2, members_1]; 174 | case 6: 175 | e_2 = _a.sent(); 176 | this.logger.error({ error: this.formatError(e_2) }, "Failed to get google members"); 177 | return [2, []]; 178 | case 7: return [2]; 179 | } 180 | }); 181 | }); 182 | }; 183 | this.getGrafanaOrgId = function (name) { return __awaiter(_this, void 0, void 0, function () { 184 | var response, e_3; 185 | return __generator(this, function (_a) { 186 | switch (_a.label) { 187 | case 0: 188 | _a.trys.push([0, 2, , 3]); 189 | this.logger.debug({ name: name }, "Get grafana organization by name."); 190 | return [4, request({ 191 | headers: { 192 | "Accept": "application/json", 193 | "Content-Type": "application/json", 194 | }, 195 | json: true, 196 | uri: this.grafanaUri + "/api/orgs/name/" + name, 197 | }).catch(function (err) { return err.response; })]; 198 | case 1: 199 | response = _a.sent(); 200 | this.logger.debug({ name: name, response: response }, "Got grafana organization by name."); 201 | if (!response.id) { 202 | throw new Error("Could not get grafana organization by name " + name); 203 | } 204 | return [2, response.id]; 205 | case 2: 206 | e_3 = _a.sent(); 207 | this.logger.error("Failed to get grafana org id", { name: name, error: this.formatError(e_3) }); 208 | return [2, ""]; 209 | case 3: return [2]; 210 | } 211 | }); 212 | }); }; 213 | this.getGrafanaOrgUsers = function (orgId, role) { return __awaiter(_this, void 0, void 0, function () { 214 | var response, e_4; 215 | return __generator(this, function (_a) { 216 | switch (_a.label) { 217 | case 0: 218 | _a.trys.push([0, 2, , 3]); 219 | this.logger.debug({ orgId: orgId }, "Get grafana organization users."); 220 | return [4, request({ 221 | headers: { 222 | "Accept": "application/json", 223 | "Content-Type": "application/json", 224 | }, 225 | json: true, 226 | uri: this.grafanaUri + "/api/orgs/" + orgId + "/users", 227 | }).catch(function (err) { return err.response; })]; 228 | case 1: 229 | response = _a.sent(); 230 | this.logger.debug({ orgId: orgId, users: response.map(function (r) { return r.email; }) }, "Got grafana organization users."); 231 | if (response.constructor !== Array) { 232 | return [2, []]; 233 | } 234 | return [2, response 235 | .filter(function (m) { return m.email && m.email !== "admin@localhost"; }) 236 | .filter(function (m) { return m.role && m.role === role; }) 237 | .map(function (m) { return m.email; })]; 238 | case 2: 239 | e_4 = _a.sent(); 240 | this.logger.error("Failed to get grafana users", { orgId: orgId, error: this.formatError(e_4) }); 241 | return [2, []]; 242 | case 3: return [2]; 243 | } 244 | }); 245 | }); }; 246 | this.getGrafanaUserId = function (email) { return __awaiter(_this, void 0, void 0, function () { 247 | var response, e_5; 248 | return __generator(this, function (_a) { 249 | switch (_a.label) { 250 | case 0: 251 | _a.trys.push([0, 2, , 3]); 252 | this.logger.debug({ email: email }, "Get grafana user id."); 253 | return [4, request({ 254 | headers: { 255 | "Accept": "application/json", 256 | "Content-Type": "application/json", 257 | }, 258 | json: true, 259 | uri: this.grafanaUri + "/api/users/lookup?loginOrEmail=" + email, 260 | }).catch(function (err) { return err.response; })]; 261 | case 1: 262 | response = _a.sent(); 263 | this.logger.debug({ email: email, response: response }, "Got grafana user id."); 264 | if (response.constructor !== Object) { 265 | throw new Error("Could not get user by email: " + email); 266 | } 267 | return [2, response.id]; 268 | case 2: 269 | e_5 = _a.sent(); 270 | this.logger.error("Failed to get grafana user by email", { email: email, error: this.formatError(e_5) }); 271 | return [2, ""]; 272 | case 3: return [2]; 273 | } 274 | }); 275 | }); }; 276 | this.getGrafanaUserRole = function (userId, orgId, email) { return __awaiter(_this, void 0, void 0, function () { 277 | var response, userOrgs, role, e_6; 278 | return __generator(this, function (_a) { 279 | switch (_a.label) { 280 | case 0: 281 | _a.trys.push([0, 2, , 3]); 282 | return [4, request({ 283 | headers: { 284 | "Accept": "application/json", 285 | "Content-Type": "application/json", 286 | }, 287 | json: true, 288 | uri: this.grafanaUri + "/api/users/" + userId + "/orgs", 289 | }).catch(function (err) { return err.response; })]; 290 | case 1: 291 | response = _a.sent(); 292 | this.logger.debug({ userId: userId, email: email, response: response }, "Got grafana user."); 293 | if (response.constructor !== Array) { 294 | throw new Error("Could not get user: " + userId); 295 | } 296 | userOrgs = response.filter(function (u) { return u.orgId.toString() === orgId.toString(); }); 297 | if (!userOrgs || userOrgs.length !== 1) { 298 | return [2, ""]; 299 | } 300 | role = userOrgs[0].role; 301 | this.logger.debug({ userId: userId, email: email, role: role }, "Got grafana user role."); 302 | return [2, role]; 303 | case 2: 304 | e_6 = _a.sent(); 305 | this.logger.error("Failed to get grafana user role", { userId: userId, error: this.formatError(e_6) }); 306 | return [2, ""]; 307 | case 3: return [2]; 308 | } 309 | }); 310 | }); }; 311 | this.createGrafanaUser = function (orgId, email, role) { return __awaiter(_this, void 0, void 0, function () { 312 | var response, e_7; 313 | return __generator(this, function (_a) { 314 | switch (_a.label) { 315 | case 0: 316 | _a.trys.push([0, 2, , 3]); 317 | this.logger.debug({ orgId: orgId, email: email, role: role }, "Create grafana user."); 318 | return [4, request({ 319 | method: "POST", 320 | headers: { 321 | "Accept": "application/json", 322 | "Content-Type": "application/json", 323 | }, 324 | body: { 325 | loginOrEmail: email, 326 | role: role, 327 | }, 328 | json: true, 329 | uri: this.grafanaUri + "/api/orgs/" + orgId + "/users", 330 | }).catch(function (err) { return err.response; })]; 331 | case 1: 332 | response = _a.sent(); 333 | this.logger.debug({ orgId: orgId, email: email, role: role, response: response }, "Created grafana organization user."); 334 | return [2, response]; 335 | case 2: 336 | e_7 = _a.sent(); 337 | this.logger.error("Failed to create grafana user", { orgId: orgId, email: email, role: role, error: this.formatError(e_7) }); 338 | return [3, 3]; 339 | case 3: return [2]; 340 | } 341 | }); 342 | }); }; 343 | this.deleteGrafanaUser = function (orgId, userId, email) { return __awaiter(_this, void 0, void 0, function () { 344 | var response, e_8; 345 | return __generator(this, function (_a) { 346 | switch (_a.label) { 347 | case 0: 348 | _a.trys.push([0, 2, , 3]); 349 | this.logger.debug({ 350 | orgId: orgId, 351 | userId: userId, 352 | email: email, 353 | }, "Delete grafana user."); 354 | return [4, request({ 355 | method: "DELETE", 356 | headers: { 357 | "Accept": "application/json", 358 | "Content-Type": "application/json", 359 | }, 360 | json: true, 361 | uri: this.grafanaUri + "/api/orgs/" + orgId + "/users/" + userId, 362 | }).catch(function (err) { return err.response; })]; 363 | case 1: 364 | response = _a.sent(); 365 | this.logger.debug({ orgId: orgId, userId: userId, response: response }, "Delete grafana user."); 366 | return [2, response]; 367 | case 2: 368 | e_8 = _a.sent(); 369 | this.logger.error("Failed to delete grafana user", { orgId: orgId, userId: userId, error: this.formatError(e_8) }); 370 | return [3, 3]; 371 | case 3: return [2]; 372 | } 373 | }); 374 | }); }; 375 | this.formatError = function (err) { 376 | if (!err) { 377 | return ""; 378 | } 379 | if (err && err.error) { 380 | return err.error; 381 | } 382 | if (err && err.message) { 383 | return err.message; 384 | } 385 | return ""; 386 | }; 387 | this.sync = function () { return __awaiter(_this, void 0, void 0, function () { 388 | var e_9; 389 | var _this = this; 390 | return __generator(this, function (_a) { 391 | switch (_a.label) { 392 | case 0: 393 | _a.trys.push([0, 6, , 7]); 394 | if (this.updateRunning) { 395 | this.logger.debug("Update is already running. Skipping..."); 396 | return [2]; 397 | } 398 | this.logger.info("Start sync process"); 399 | this.updateRunning = true; 400 | return [4, Promise.all(this.rules.map(function (rule) { return __awaiter(_this, void 0, void 0, function () { 401 | var groupEmail, orgName, role, orgId, uniqueId, _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, e_10; 402 | return __generator(this, function (_l) { 403 | switch (_l.label) { 404 | case 0: 405 | _l.trys.push([0, 4, , 5]); 406 | groupEmail = rule.split(":")[0]; 407 | orgName = rule.split(":")[1]; 408 | role = rule.split(":")[2]; 409 | if (!groupEmail || !orgName || !role) { 410 | throw new Error("Email or organization name or role missing."); 411 | } 412 | return [4, this.getGrafanaOrgId(orgName)]; 413 | case 1: 414 | orgId = _l.sent(); 415 | if (!orgId) { 416 | throw new Error("Could not get grafana organization"); 417 | } 418 | uniqueId = orgId + ":" + role; 419 | _b = (_a = this.grafanaMembers).set; 420 | _c = [uniqueId]; 421 | _e = (_d = (this.grafanaMembers.get(uniqueId) || [])).concat; 422 | return [4, this.getGrafanaOrgUsers(orgId, role)]; 423 | case 2: 424 | _b.apply(_a, _c.concat([_e.apply(_d, [_l.sent()])])); 425 | _g = (_f = this.googleMembers).set; 426 | _h = [uniqueId]; 427 | _k = (_j = (this.googleMembers.get(uniqueId) || [])).concat; 428 | return [4, this.getGroupMembers(groupEmail)]; 429 | case 3: 430 | _g.apply(_f, _h.concat([_k.apply(_j, [_l.sent()])])); 431 | this.success.inc(); 432 | return [3, 5]; 433 | case 4: 434 | e_10 = _l.sent(); 435 | this.fail.inc(); 436 | this.logger.error("Failed to build grafana and google users cache", this.formatError(e_10)); 437 | return [3, 5]; 438 | case 5: return [2]; 439 | } 440 | }); 441 | }); }))]; 442 | case 1: 443 | _a.sent(); 444 | this.logger.debug(this.googleMembers, "Google members map before create/update"); 445 | this.logger.debug(this.grafanaMembers, "Grafana members map before create/update"); 446 | return [4, Promise.all(Array.from(this.googleMembers.keys()).map(function (uniqueId) { return __awaiter(_this, void 0, void 0, function () { 447 | var emails, orgId, role; 448 | var _this = this; 449 | return __generator(this, function (_a) { 450 | switch (_a.label) { 451 | case 0: 452 | emails = this.googleMembers.get(uniqueId); 453 | orgId = uniqueId.split(":")[0]; 454 | role = uniqueId.split(":")[1]; 455 | return [4, Promise.all(emails.map(function (email) { return __awaiter(_this, void 0, void 0, function () { 456 | var userId, e_11; 457 | return __generator(this, function (_a) { 458 | switch (_a.label) { 459 | case 0: 460 | _a.trys.push([0, 6, 7, 8]); 461 | this.logger.info({ email: email, orgId: orgId, role: role }, "Sync gsuite rule"); 462 | return [4, this.getGrafanaUserId(email)]; 463 | case 1: 464 | userId = _a.sent(); 465 | if (!userId) return [3, 5]; 466 | if (!!this.grafanaMembers.get(uniqueId).find(function (e) { return e === email; })) return [3, 3]; 467 | return [4, this.createGrafanaUser(orgId, email, role)]; 468 | case 2: 469 | _a.sent(); 470 | return [3, 5]; 471 | case 3: return [4, this.updateGrafanaUser(orgId, userId, role, email)]; 472 | case 4: 473 | _a.sent(); 474 | _a.label = 5; 475 | case 5: return [3, 8]; 476 | case 6: 477 | e_11 = _a.sent(); 478 | this.logger.error("Failed to create or update all google users in grafana", this.formatError(e_11)); 479 | return [3, 8]; 480 | case 7: 481 | this.logger.debug("Remove user " + email + " from sync map."); 482 | this.grafanaMembers.set(uniqueId, this.grafanaMembers.get(uniqueId).filter(function (e) { return e !== email; })); 483 | return [7]; 484 | case 8: return [2]; 485 | } 486 | }); 487 | }); }))]; 488 | case 1: 489 | _a.sent(); 490 | return [2]; 491 | } 492 | }); 493 | }); }))]; 494 | case 2: 495 | _a.sent(); 496 | this.logger.debug(this.googleMembers, "Google members map before delete"); 497 | this.logger.debug(this.grafanaMembers, "Grafana members map before delete"); 498 | if (!(this.mode === "sync")) return [3, 4]; 499 | return [4, Promise.all(Array.from(this.grafanaMembers.keys()).map(function (uniqueId) { return __awaiter(_this, void 0, void 0, function () { 500 | var emails, orgId; 501 | var _this = this; 502 | return __generator(this, function (_a) { 503 | switch (_a.label) { 504 | case 0: 505 | emails = this.grafanaMembers.get(uniqueId); 506 | orgId = uniqueId.split(":")[0]; 507 | return [4, Promise.all(emails.map(function (email) { return __awaiter(_this, void 0, void 0, function () { 508 | var userId, userRole; 509 | return __generator(this, function (_a) { 510 | switch (_a.label) { 511 | case 0: return [4, this.getGrafanaUserId(email)]; 512 | case 1: 513 | userId = _a.sent(); 514 | if (!userId) return [3, 4]; 515 | return [4, this.getGrafanaUserRole(userId, orgId, email)]; 516 | case 2: 517 | userRole = _a.sent(); 518 | if (!(this.excludeRole !== userRole && !this.googleMembers.get(uniqueId).find(function (e) { return e === email; }))) return [3, 4]; 519 | return [4, this.deleteGrafanaUser(orgId, userId, email)]; 520 | case 3: 521 | _a.sent(); 522 | _a.label = 4; 523 | case 4: return [2]; 524 | } 525 | }); 526 | }); }))]; 527 | case 1: 528 | _a.sent(); 529 | return [2]; 530 | } 531 | }); 532 | }); }))]; 533 | case 3: 534 | _a.sent(); 535 | _a.label = 4; 536 | case 4: return [4, Promise.all(this.staticRules.map(function (rule) { return __awaiter(_this, void 0, void 0, function () { 537 | var email, orgName, role, orgId, uniqueId, userId, e_12, e_13; 538 | return __generator(this, function (_a) { 539 | switch (_a.label) { 540 | case 0: 541 | email = rule.split(":")[0]; 542 | orgName = rule.split(":")[1]; 543 | role = rule.split(":")[2]; 544 | if (!email || !orgName || !role) { 545 | throw new Error("Email or organization name or role missing."); 546 | } 547 | return [4, this.getGrafanaOrgId(orgName)]; 548 | case 1: 549 | orgId = _a.sent(); 550 | if (!orgId) { 551 | throw new Error("Could not get grafana organization"); 552 | } 553 | this.logger.info({ email: email, orgId: orgId, role: role }, "Sync static rule"); 554 | uniqueId = orgId + ":" + role; 555 | _a.label = 2; 556 | case 2: 557 | _a.trys.push([2, 9, 10, 11]); 558 | return [4, this.getGrafanaUserId(email)]; 559 | case 3: 560 | userId = _a.sent(); 561 | if (!userId) return [3, 8]; 562 | _a.label = 4; 563 | case 4: 564 | _a.trys.push([4, 6, , 8]); 565 | return [4, this.createGrafanaUser(orgId, email, role)]; 566 | case 5: 567 | _a.sent(); 568 | return [3, 8]; 569 | case 6: 570 | e_12 = _a.sent(); 571 | return [4, this.updateGrafanaUser(orgId, userId, role, email)]; 572 | case 7: 573 | _a.sent(); 574 | return [3, 8]; 575 | case 8: return [3, 11]; 576 | case 9: 577 | e_13 = _a.sent(); 578 | this.logger.error("Failed to create or update static users", this.formatError(e_13)); 579 | return [3, 11]; 580 | case 10: 581 | if (this.grafanaMembers.get(uniqueId)) { 582 | this.logger.debug("Remove user " + email + " from sync map."); 583 | this.grafanaMembers.set(uniqueId, this.grafanaMembers.get(uniqueId).filter(function (e) { return e !== email; })); 584 | } 585 | return [7]; 586 | case 11: return [2]; 587 | } 588 | }); 589 | }); }))]; 590 | case 5: 591 | _a.sent(); 592 | this.googleMembers.clear(); 593 | this.grafanaMembers.clear(); 594 | this.logger.info("End sync process"); 595 | this.updateRunning = false; 596 | return [3, 7]; 597 | case 6: 598 | e_9 = _a.sent(); 599 | this.fail.inc(); 600 | this.logger.error(this.formatError(e_9)); 601 | this.updateRunning = false; 602 | return [3, 7]; 603 | case 7: return [2]; 604 | } 605 | }); 606 | }); }; 607 | this.updateRunning = false; 608 | this.logLevel = process.env.LEVEL || commander.level || "info"; 609 | this.logger = pino({ 610 | prettyPrint: process.env.NODE_ENV !== "production", 611 | level: this.logLevel, 612 | }); 613 | this.grafanaProtocol = process.env.GRAFANA_PROTOCOL || commander.grafanaProtocol || "http"; 614 | this.grafanaHost = process.env.GRAFANA_HOST || commander.grafanaHost || "localhost:3000"; 615 | this.grafanaUsername = process.env.GRAFANA_USERNAME || commander.grafanaUsername || "admin"; 616 | this.grafanaPassword = process.env.GRAFANA_PASSWORD || commander.grafanaPassword || ""; 617 | this.grafanaUri = this.grafanaProtocol + "://" + this.grafanaUsername + ":" + this.grafanaPassword + "@" + this.grafanaHost; 618 | this.credentialsPath = process.env.GOOGLE_CREDENTIALS || commander.googleCredentials || ".credentials.json"; 619 | this.googleAdminEmail = process.env.GOOGLE_ADMIN_EMAIL || commander.googleAdminEmail || ""; 620 | this.rules = process.env.RULES || commander.rules || []; 621 | this.staticRules = process.env.STATIC_RULES || commander.staticRules || []; 622 | this.mode = process.env.MODE || commander.mode || "sync"; 623 | this.excludeRole = process.env.EXCLUDE_ROLE || commander.excludeRole || ""; 624 | this.metricsInterval = prom_client_1.collectDefaultMetrics(); 625 | this.success = new prom_client_1.Counter({ 626 | help: "Successful grafana gsuite sync counter", 627 | name: "grafana_gsuite_sync_success", 628 | }); 629 | this.fail = new prom_client_1.Counter({ 630 | help: "Unsuccessful grafana gsuite sync counter", 631 | name: "grafana_gsuite_sync_fail", 632 | }); 633 | this.grafanaMembers = new Map(); 634 | this.googleMembers = new Map(); 635 | } 636 | GrafanaSync.prototype.updateGrafanaUser = function (orgId, userId, role, email) { 637 | return __awaiter(this, void 0, void 0, function () { 638 | var oldRole, response, e_14; 639 | return __generator(this, function (_a) { 640 | switch (_a.label) { 641 | case 0: 642 | _a.trys.push([0, 3, , 4]); 643 | return [4, this.getGrafanaUserRole(userId, orgId, email)]; 644 | case 1: 645 | oldRole = _a.sent(); 646 | if (oldRole === role) { 647 | this.logger.debug({ orgId: orgId, email: email, role: role }, "The role is already set, so skipping user update"); 648 | return [2]; 649 | } 650 | if (oldRole === "Admin" && (role === "Editor" || role === "Viewer")) { 651 | this.logger.debug({ orgId: orgId, email: email, role: role }, "The existing role is more powerful, so skipping user update"); 652 | return [2]; 653 | } 654 | if (oldRole === "Editor" && role === "Viewer") { 655 | this.logger.debug({ orgId: orgId, email: email, role: role }, "The existing role is more powerful, so skipping user update"); 656 | return [2]; 657 | } 658 | this.logger.debug({ orgId: orgId, userId: userId, role: role }, "Updating grafana user."); 659 | return [4, request({ 660 | method: "PATCH", 661 | headers: { 662 | "Accept": "application/json", 663 | "Content-Type": "application/json", 664 | }, 665 | body: { 666 | role: role, 667 | }, 668 | json: true, 669 | uri: this.grafanaUri + "/api/orgs/" + orgId + "/users/" + userId, 670 | }).catch(function (err) { return err.response; })]; 671 | case 2: 672 | response = _a.sent(); 673 | this.logger.debug({ orgId: orgId, userId: userId, role: role, response: response }, "Updated grafana user."); 674 | return [2, response]; 675 | case 3: 676 | e_14 = _a.sent(); 677 | this.logger.error("Failed to update grafana user", { orgId: orgId, userId: userId, role: role, error: this.formatError(e_14) }); 678 | return [3, 4]; 679 | case 4: return [2]; 680 | } 681 | }); 682 | }); 683 | }; 684 | return GrafanaSync; 685 | }()); 686 | var grafanaSync = new GrafanaSync(); 687 | app.get("/healthz", function (req, res) { 688 | res.status(200).json({ status: "UP" }); 689 | }); 690 | app.get("/metrics", function (req, res) { return __awaiter(void 0, void 0, void 0, function () { 691 | return __generator(this, function (_a) { 692 | try { 693 | res.set("Content-Type", prom_client_1.register.contentType); 694 | res.end(prom_client_1.register.metrics()); 695 | } 696 | catch (e) { 697 | res.status(503).json({ error: e.toString() }); 698 | } 699 | return [2]; 700 | }); 701 | }); }); 702 | var server = app.listen(port, function () { 703 | console.info("Server listening on port " + port + "!"); 704 | return grafanaSync.sync(); 705 | }); 706 | var interval = process.env.INTERVAL || commander.interval || 24 * 60 * 60 * 1000; 707 | var updateInterval = setInterval(grafanaSync.sync, parseInt(interval, 10)); 708 | process.on("SIGTERM", function () { 709 | clearInterval(updateInterval); 710 | server.close(function (err) { 711 | if (err) { 712 | console.error(err); 713 | process.exit(1); 714 | } 715 | process.exit(0); 716 | }); 717 | }); 718 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /draw.xml: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /examples/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1beta1 3 | kind: Deployment 4 | metadata: 5 | name: grafana-gsuite-sync 6 | spec: 7 | replicas: 1 8 | template: 9 | metadata: 10 | name: grafana-gsuite-sync 11 | annotations: 12 | prometheus.io/scrape: 'true' 13 | prometheus.io/port: '5000' 14 | prometheus.io/path: '/metrics' 15 | labels: 16 | app: grafana-gsuite-sync 17 | spec: 18 | containers: 19 | - name: grafana-gsuite-sync 20 | image: quay.io/google-cloud-tools/grafana-gsuite-sync 21 | imagePullPolicy: Always 22 | args: 23 | - '--interval=86400000' 24 | - '--rules=test@example.com:Main:Editor' 25 | - '--static-rules=user@example.com:Main:Viewer' 26 | - '--mode=sync' 27 | - '--exclude-role=Admin' 28 | - '--grafana-protocol=http' 29 | - '--grafana-host=play.grafana.org' 30 | - '--grafana-username=admin' 31 | - '--grafana-password=admin' 32 | # - '--google-admin-email=admin@example.com' 33 | # - '--google-credentials=/secrets/credentials.json' 34 | livenessProbe: 35 | httpGet: 36 | path: /healthz 37 | port: http 38 | resources: 39 | requests: 40 | cpu: 0 41 | memory: 100Mi 42 | limits: 43 | cpu: 0.2 44 | memory: 500Mi 45 | # volumeMounts: 46 | # - name: google-admin-api-credentials 47 | # mountPath: /secrets 48 | # readOnly: true 49 | # volumes: 50 | # - name: google-admin-api-credentials 51 | # secret: 52 | # secretName: google-admin-api-credentials 53 | # items: 54 | # - key: credentials.json 55 | # path: credentials.json 56 | -------------------------------------------------------------------------------- /graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudworkz/grafana-gsuite-sync/77810f3cb670a316975f3f5c7fe0d58ba0df0284/graph.png -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as commander from "commander"; 4 | import * as express from "express"; 5 | import { readFile } from "fs"; 6 | import { auth } from "google-auth-library"; 7 | import { admin_directory_v1, google } from "googleapis"; 8 | import * as pino from "pino"; 9 | import { collectDefaultMetrics, Counter, register } from "prom-client"; 10 | import * as request from "request-promise"; 11 | import { promisify } from "util"; 12 | 13 | const readFileAsync = promisify(readFile); 14 | 15 | const collect = (value: string | string[], previous: string[]) => { 16 | const splitValue = Array.isArray(value) ? value : value.split(","); 17 | return previous.concat(splitValue); 18 | }; 19 | 20 | commander 21 | .option("-p, --port [port]", "Server port") 22 | .option("-P, --grafana-protocol [grafana-protocol]", "Grafana API protocol", /^(http|https)$/i) 23 | .option("-H, --grafana-host [grafana-host]", "Grafana API host") 24 | .option("-U, --grafana-username [grafana-username]", "Grafana API admin username", "") 25 | .option("-P, --grafana-password ", "Grafana API admin password", "") 26 | .option("-C, --google-credentials ", "Path to google admin directory credentials file", "") 27 | .option("-A, --google-admin-email ", "The Google Admin Email for subject", "") 28 | .option( 29 | "-r, --rules ", 30 | "Comma separated or repeatable rules to sync :: \n\t" + 31 | "(e.g. 'group@test.com:Main:Admin')", 32 | collect, 33 | [], 34 | ) 35 | .option( 36 | "-s, --static-rules ", 37 | "Comma separated or repeatable static rules to create :: \n\t" + 38 | "(e.g. 'user@test.com:Main:Viewer')", 39 | collect, 40 | [], 41 | ) 42 | .option("-l, --level [level]", "Log level", /^(debug|info|warn|error|fatal)$/i) 43 | .option("-m, --mode [mode]", 44 | "How users are sychronized between google and grafana: sync or upsert-only", /^(sync|upsert-only)$/i) 45 | .option("-e, --exclude-role [exclude-role]", "Exclude role to delete", /^(Admin|Editor|Viewer)$/i) 46 | .option("-i, --interval [interval]", "Sync interval") 47 | .parse(process.argv); 48 | 49 | const app = express(); 50 | const port = process.env.PORT || commander.port || 5000; 51 | 52 | class GrafanaSync { 53 | public service: admin_directory_v1.Admin; 54 | public client: any; 55 | public updateRunning: boolean; 56 | public logLevel: string; 57 | public logger: pino.Logger; 58 | public grafanaProtocol: string; 59 | public grafanaHost: string; 60 | public grafanaUsername: string; 61 | public grafanaPassword: string; 62 | public grafanaUri: string; 63 | public credentialsPath: string; 64 | public googleAdminEmail: string; 65 | public rules: string[]; 66 | public staticRules: string[]; 67 | public mode: string; 68 | public excludeRole: string; 69 | public interval: number; 70 | public metricsInterval: NodeJS.Timeout; 71 | public success: Counter; 72 | public fail: Counter; 73 | public grafanaMembers: Map; 74 | public googleMembers: Map; 75 | 76 | constructor() { 77 | this.updateRunning = false; 78 | this.logLevel = process.env.LEVEL || commander.level || "info"; 79 | this.logger = pino({ 80 | prettyPrint: process.env.NODE_ENV !== "production", 81 | level: this.logLevel, 82 | }); 83 | 84 | this.grafanaProtocol = process.env.GRAFANA_PROTOCOL || commander.grafanaProtocol || "http"; 85 | this.grafanaHost = process.env.GRAFANA_HOST || commander.grafanaHost || "localhost:3000"; 86 | this.grafanaUsername = process.env.GRAFANA_USERNAME || commander.grafanaUsername || "admin"; 87 | this.grafanaPassword = process.env.GRAFANA_PASSWORD || commander.grafanaPassword || ""; 88 | this.grafanaUri = `${this.grafanaProtocol}://${this.grafanaUsername}:${this.grafanaPassword}@${this.grafanaHost}`; 89 | 90 | this.credentialsPath = process.env.GOOGLE_CREDENTIALS || commander.googleCredentials || ".credentials.json"; 91 | this.googleAdminEmail = process.env.GOOGLE_ADMIN_EMAIL || commander.googleAdminEmail || ""; 92 | this.rules = process.env.RULES || commander.rules || []; 93 | this.staticRules = process.env.STATIC_RULES || commander.staticRules || []; 94 | this.mode = process.env.MODE || commander.mode || "sync"; 95 | this.excludeRole = process.env.EXCLUDE_ROLE || commander.excludeRole || ""; 96 | 97 | this.metricsInterval = collectDefaultMetrics(); 98 | this.success = new Counter({ 99 | help: "Successful grafana gsuite sync counter", 100 | name: "grafana_gsuite_sync_success", 101 | }); 102 | this.fail = new Counter({ 103 | help: "Unsuccessful grafana gsuite sync counter", 104 | name: "grafana_gsuite_sync_fail", 105 | }); 106 | this.grafanaMembers = new Map(); // { orgId: ["email1","email2"]} 107 | this.googleMembers = new Map(); // { orgId: ["email1","email2"]} 108 | } 109 | 110 | public getGoogleApiClient = async () => { 111 | if (this.service && this.client) { 112 | return; 113 | } 114 | try { 115 | this.logger.debug("Get google api client"); 116 | const content = await readFileAsync(this.credentialsPath); 117 | const credentials = JSON.parse(content.toString()); 118 | const client: any = auth.fromJSON(credentials, {}); 119 | client.subject = this.googleAdminEmail; 120 | client.scopes = [ 121 | "https://www.googleapis.com/auth/admin.directory.group.member.readonly", 122 | "https://www.googleapis.com/auth/admin.directory.group.readonly", 123 | ]; 124 | await client.authorize(); 125 | this.client = client; 126 | this.service = google.admin("directory_v1"); 127 | } catch (e) { 128 | this.logger.error("Failed to get google api client", { error: this.formatError(e) }); 129 | } 130 | } 131 | 132 | public getGroupMembers = async (email: string, nextPageToken: string = "") => { 133 | try { 134 | await this.getGoogleApiClient(); 135 | if (!this.service || !this.client) { 136 | this.logger.debug("The google api is not configured."); 137 | return []; 138 | } 139 | const options: any = { 140 | auth: this.client, 141 | groupKey: email, 142 | }; 143 | if (nextPageToken) { 144 | options.pageToken = nextPageToken; 145 | } 146 | const response = await this.service.members.list(options); 147 | 148 | if (response.status !== 200 || !response.data || !response.data.members) { 149 | throw new Error("Failed to get members list."); 150 | } 151 | this.logger.debug("Got google response members", { members: response.data.members.map((m) => m.email) }); 152 | let members = []; 153 | await Promise.all(response.data.members.filter((m) => m.email).map(async (member) => { 154 | if (member.type === "GROUP") { 155 | const subMembers = await this.getGroupMembers(member.email); 156 | members = members.concat(subMembers); 157 | } else { 158 | members.push(member.email); 159 | } 160 | })); 161 | if (response.data.nextPageToken) { 162 | this.logger.debug("Find next page"); 163 | const pageMembers = await this.getGroupMembers(email, response.data.nextPageToken); 164 | this.logger.debug("Got google page members", { members: pageMembers }); 165 | members = members.concat(pageMembers); 166 | } 167 | this.logger.debug({ members }, "Got google members"); 168 | return members; 169 | } catch (e) { 170 | this.logger.error({ error: this.formatError(e) }, "Failed to get google members"); 171 | return []; 172 | } 173 | } 174 | 175 | public getGrafanaOrgId = async (name: string) => { 176 | try { 177 | this.logger.debug({ name }, "Get grafana organization by name."); 178 | const response = await request({ 179 | headers: { 180 | "Accept": "application/json", 181 | "Content-Type": "application/json", 182 | }, 183 | json: true, 184 | uri: `${this.grafanaUri}/api/orgs/name/${name}`, 185 | }).catch((err) => err.response); 186 | this.logger.debug({ name, response }, "Got grafana organization by name."); 187 | if (!response.id) { 188 | throw new Error(`Could not get grafana organization by name ${name}`); 189 | } 190 | return response.id; 191 | } catch (e) { 192 | this.logger.error("Failed to get grafana org id", { name, error: this.formatError(e) }); 193 | return ""; 194 | } 195 | } 196 | 197 | public getGrafanaOrgUsers = async (orgId: string, role: string) => { 198 | try { 199 | this.logger.debug({ orgId }, "Get grafana organization users."); 200 | const response = await request({ 201 | headers: { 202 | "Accept": "application/json", 203 | "Content-Type": "application/json", 204 | }, 205 | json: true, 206 | uri: `${this.grafanaUri}/api/orgs/${orgId}/users`, 207 | }).catch((err) => err.response); 208 | this.logger.debug({ orgId, users: response.map((r) => r.email) }, "Got grafana organization users."); 209 | if (response.constructor !== Array) { 210 | return []; 211 | } 212 | return response 213 | .filter((m) => m.email && m.email !== "admin@localhost") 214 | .filter((m) => m.role && m.role === role) 215 | .map((m) => m.email); 216 | } catch (e) { 217 | this.logger.error("Failed to get grafana users", { orgId, error: this.formatError(e) }); 218 | return []; 219 | } 220 | } 221 | 222 | public getGrafanaUserId = async (email: string) => { 223 | try { 224 | this.logger.debug({ email }, "Get grafana user id."); 225 | const response = await request({ 226 | headers: { 227 | "Accept": "application/json", 228 | "Content-Type": "application/json", 229 | }, 230 | json: true, 231 | uri: `${this.grafanaUri}/api/users/lookup?loginOrEmail=${email}`, 232 | }).catch((err) => err.response); 233 | this.logger.debug({ email, response }, "Got grafana user id."); 234 | if (response.constructor !== Object) { 235 | throw new Error(`Could not get user by email: ${email}`); 236 | } 237 | return response.id; 238 | } catch (e) { 239 | this.logger.error("Failed to get grafana user by email", { email, error: this.formatError(e) }); 240 | return ""; 241 | } 242 | } 243 | 244 | public getGrafanaUserRole = async (userId: string, orgId: string, email: string) => { 245 | try { 246 | const response = await request({ 247 | headers: { 248 | "Accept": "application/json", 249 | "Content-Type": "application/json", 250 | }, 251 | json: true, 252 | uri: `${this.grafanaUri}/api/users/${userId}/orgs`, 253 | }).catch((err) => err.response); 254 | this.logger.debug({ userId, email, response }, "Got grafana user."); 255 | if (response.constructor !== Array) { 256 | throw new Error(`Could not get user: ${userId}`); 257 | } 258 | const userOrgs = response.filter((u) => u.orgId.toString() === orgId.toString()); 259 | if (!userOrgs || userOrgs.length !== 1) { 260 | return ""; 261 | } 262 | const role = userOrgs[0].role; 263 | this.logger.debug({ userId, email, role }, "Got grafana user role."); 264 | return role; 265 | } catch (e) { 266 | this.logger.error("Failed to get grafana user role", { userId, error: this.formatError(e) }); 267 | return ""; 268 | } 269 | } 270 | 271 | public createGrafanaUser = async (orgId: string, email: string, role: string) => { 272 | // Only works if the user already signed up e.g. Google Auth 273 | try { 274 | this.logger.debug({ orgId, email, role }, "Create grafana user."); 275 | const response = await request({ 276 | method: "POST", 277 | headers: { 278 | "Accept": "application/json", 279 | "Content-Type": "application/json", 280 | }, 281 | body: { 282 | loginOrEmail: email, 283 | role, 284 | }, 285 | json: true, 286 | uri: `${this.grafanaUri}/api/orgs/${orgId}/users`, 287 | }).catch((err) => err.response); 288 | this.logger.debug({ orgId, email, role, response }, "Created grafana organization user."); 289 | return response; 290 | } catch (e) { 291 | this.logger.error("Failed to create grafana user", { orgId, email, role, error: this.formatError(e) }); 292 | } 293 | } 294 | 295 | public async updateGrafanaUser(orgId: string, userId: string, role: string, email: string) { 296 | try { 297 | const oldRole = await this.getGrafanaUserRole(userId, orgId, email); 298 | if (oldRole === role) { 299 | this.logger.debug({ orgId, email, role }, "The role is already set, so skipping user update"); 300 | return; 301 | } 302 | if (oldRole === "Admin" && (role === "Editor" || role === "Viewer")) { 303 | this.logger.debug({ orgId, email, role }, "The existing role is more powerful, so skipping user update"); 304 | return; 305 | } 306 | if (oldRole === "Editor" && role === "Viewer") { 307 | this.logger.debug({ orgId, email, role }, "The existing role is more powerful, so skipping user update"); 308 | return; 309 | } 310 | this.logger.debug({ orgId, userId, role }, "Updating grafana user."); 311 | const response = await request({ 312 | method: "PATCH", 313 | headers: { 314 | "Accept": "application/json", 315 | "Content-Type": "application/json", 316 | }, 317 | body: { 318 | role, 319 | }, 320 | json: true, 321 | uri: `${this.grafanaUri}/api/orgs/${orgId}/users/${userId}`, 322 | }).catch((err) => err.response); 323 | this.logger.debug({ orgId, userId, role, response }, "Updated grafana user."); 324 | return response; 325 | } catch (e) { 326 | this.logger.error("Failed to update grafana user", { orgId, userId, role, error: this.formatError(e) }); 327 | } 328 | } 329 | 330 | public deleteGrafanaUser = async (orgId: string, userId: string, email: string) => { 331 | try { 332 | this.logger.debug({ 333 | orgId, 334 | userId, 335 | email, 336 | }, "Delete grafana user."); 337 | const response = await request({ 338 | method: "DELETE", 339 | headers: { 340 | "Accept": "application/json", 341 | "Content-Type": "application/json", 342 | }, 343 | json: true, 344 | uri: `${this.grafanaUri}/api/orgs/${orgId}/users/${userId}`, 345 | }).catch((err) => err.response); 346 | this.logger.debug({ orgId, userId, response }, "Delete grafana user."); 347 | return response; 348 | } catch (e) { 349 | this.logger.error("Failed to delete grafana user", { orgId, userId, error: this.formatError(e) }); 350 | } 351 | } 352 | 353 | public formatError = (err: any) => { 354 | if (!err) { 355 | return ""; 356 | } 357 | if (err && err.error) { 358 | return err.error; 359 | } 360 | if (err && err.message) { 361 | return err.message; 362 | } 363 | return ""; 364 | } 365 | 366 | public sync = async () => { 367 | try { 368 | if (this.updateRunning) { 369 | this.logger.debug("Update is already running. Skipping..."); 370 | return; 371 | } 372 | this.logger.info("Start sync process"); 373 | this.updateRunning = true; 374 | 375 | // Build grafana and google users cache 376 | await Promise.all(this.rules.map(async (rule) => { 377 | try { 378 | const groupEmail = rule.split(":")[0]; 379 | const orgName = rule.split(":")[1]; 380 | const role = rule.split(":")[2]; 381 | if (!groupEmail || !orgName || !role) { 382 | throw new Error("Email or organization name or role missing."); 383 | } 384 | 385 | const orgId = await this.getGrafanaOrgId(orgName); 386 | if (!orgId) { 387 | throw new Error("Could not get grafana organization"); 388 | } 389 | const uniqueId = `${orgId}:${role}`; 390 | this.grafanaMembers.set(uniqueId, (this.grafanaMembers.get(uniqueId) || []).concat(await this.getGrafanaOrgUsers(orgId, role))); 391 | 392 | this.googleMembers.set(uniqueId, (this.googleMembers.get(uniqueId) || []).concat(await this.getGroupMembers(groupEmail))); 393 | 394 | this.success.inc(); 395 | 396 | } catch (e) { 397 | this.fail.inc(); 398 | this.logger.error("Failed to build grafana and google users cache", this.formatError(e)); 399 | } 400 | })); 401 | 402 | this.logger.debug(this.googleMembers, "Google members map before create/update"); 403 | this.logger.debug(this.grafanaMembers, "Grafana members map before create/update"); 404 | 405 | // create or update all google users in grafana 406 | await Promise.all(Array.from(this.googleMembers.keys()).map(async (uniqueId) => { 407 | const emails = this.googleMembers.get(uniqueId); 408 | const orgId = uniqueId.split(":")[0]; 409 | const role = uniqueId.split(":")[1]; 410 | await Promise.all(emails.map(async (email) => { 411 | try { 412 | this.logger.info({ email, orgId, role }, "Sync gsuite rule"); 413 | const userId = await this.getGrafanaUserId(email); 414 | if (userId) { 415 | if (!this.grafanaMembers.get(uniqueId).find((e) => e === email)) { 416 | await this.createGrafanaUser(orgId, email, role); 417 | } else { 418 | await this.updateGrafanaUser(orgId, userId, role, email); 419 | } 420 | } 421 | } catch (e) { 422 | this.logger.error("Failed to create or update all google users in grafana", this.formatError(e)); 423 | } finally { 424 | this.logger.debug(`Remove user ${email} from sync map.`); 425 | this.grafanaMembers.set(uniqueId, this.grafanaMembers.get(uniqueId).filter((e) => e !== email)); 426 | } 427 | })); 428 | })); 429 | 430 | this.logger.debug(this.googleMembers, "Google members map before delete"); 431 | this.logger.debug(this.grafanaMembers, "Grafana members map before delete"); 432 | 433 | // delete users which are not in google groups 434 | if (this.mode === "sync") { 435 | await Promise.all(Array.from(this.grafanaMembers.keys()).map(async (uniqueId) => { 436 | const emails = this.grafanaMembers.get(uniqueId); 437 | const orgId = uniqueId.split(":")[0]; 438 | await Promise.all(emails.map(async (email) => { 439 | const userId = await this.getGrafanaUserId(email); 440 | if (userId) { 441 | const userRole = await this.getGrafanaUserRole(userId, orgId, email); 442 | if (this.excludeRole !== userRole && !this.googleMembers.get(uniqueId).find((e) => e === email)) { 443 | await this.deleteGrafanaUser(orgId, userId, email); 444 | } 445 | } 446 | })); 447 | })); 448 | } 449 | 450 | // create or update static users 451 | await Promise.all(this.staticRules.map(async (rule) => { 452 | const email = rule.split(":")[0]; 453 | const orgName = rule.split(":")[1]; 454 | const role = rule.split(":")[2]; 455 | if (!email || !orgName || !role) { 456 | throw new Error("Email or organization name or role missing."); 457 | } 458 | const orgId = await this.getGrafanaOrgId(orgName); 459 | if (!orgId) { 460 | throw new Error("Could not get grafana organization"); 461 | } 462 | this.logger.info({ email, orgId, role }, "Sync static rule"); 463 | const uniqueId = `${orgId}:${role}`; 464 | try { 465 | const userId = await this.getGrafanaUserId(email); 466 | if (userId) { 467 | try { 468 | await this.createGrafanaUser(orgId, email, role); 469 | } catch (e) { 470 | await this.updateGrafanaUser(orgId, userId, role, email); 471 | } 472 | } 473 | } catch (e) { 474 | this.logger.error("Failed to create or update static users", this.formatError(e)); 475 | } finally { 476 | if (this.grafanaMembers.get(uniqueId)) { 477 | this.logger.debug(`Remove user ${email} from sync map.`); 478 | this.grafanaMembers.set(uniqueId, this.grafanaMembers.get(uniqueId).filter((e) => e !== email)); 479 | } 480 | } 481 | })); 482 | 483 | this.googleMembers.clear(); 484 | this.grafanaMembers.clear(); 485 | this.logger.info("End sync process"); 486 | this.updateRunning = false; 487 | } catch (e) { 488 | this.fail.inc(); 489 | this.logger.error(this.formatError(e)); 490 | this.updateRunning = false; 491 | } 492 | } 493 | } 494 | 495 | const grafanaSync = new GrafanaSync(); 496 | 497 | app.get("/healthz", (req, res) => { 498 | res.status(200).json({ status: "UP" }); 499 | }); 500 | 501 | app.get("/metrics", async (req, res) => { 502 | try { 503 | res.set("Content-Type", register.contentType); 504 | res.end(register.metrics()); 505 | } catch (e) { 506 | res.status(503).json({ error: e.toString() }); 507 | } 508 | }); 509 | 510 | const server = app.listen(port, () => { 511 | console.info(`Server listening on port ${port}!`); 512 | return grafanaSync.sync(); 513 | }); 514 | 515 | const interval = process.env.INTERVAL || commander.interval || 24 * 60 * 60 * 1000; 516 | const updateInterval = setInterval(grafanaSync.sync, parseInt(interval, 10)); 517 | 518 | process.on("SIGTERM", () => { 519 | clearInterval(updateInterval); 520 | 521 | server.close((err) => { 522 | if (err) { 523 | console.error(err); 524 | process.exit(1); 525 | } 526 | 527 | process.exit(0); 528 | }); 529 | }); 530 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grafana-gsuite-sync", 3 | "version": "1.2.8", 4 | "description": "Synchronize G Suite users with Grafana", 5 | "main": "src/index.js", 6 | "repository": "git@github.com:yacut/grafana-gsuite-sync.git", 7 | "author": "yacut", 8 | "license": "MIT", 9 | "bin": { 10 | "anon-kafka-mirror": "dist/index.js" 11 | }, 12 | "scripts": { 13 | "prepublish": "npm run build", 14 | "build": "npm run clean && npm run tsc", 15 | "clean": "rimraf dist", 16 | "start": "node dist/index.js", 17 | "lint": "tslint \"**/*.ts\" --project tsconfig.json", 18 | "test": "NODE_ENV=test nyc _mocha && nyc check-coverage", 19 | "tsc": "tsc" 20 | }, 21 | "dependencies": { 22 | "commander": "^2.20.0", 23 | "express": "^4.17.1", 24 | "google-auth-library": "^4.2.2", 25 | "googleapis": "^40.0.0", 26 | "kubernetes-client": "^8.2.0", 27 | "pino": "^5.12.6", 28 | "pino-pretty": "^3.2.1", 29 | "prom-client": "^11.5.2", 30 | "request": "^2.88.0", 31 | "request-promise": "^4.2.4" 32 | }, 33 | "devDependencies": { 34 | "@types/chai": "^4.1.7", 35 | "@types/chai-as-promised": "^7.1.0", 36 | "@types/express": "^4.17.0", 37 | "@types/mocha": "^5.2.7", 38 | "@types/node": "^12.0.8", 39 | "@types/pino": "^5.8.8", 40 | "@types/request-promise": "^4.1.44", 41 | "chai": "^4.2.0", 42 | "chai-as-promised": "^7.1.1", 43 | "mocha": "^6.1.4", 44 | "nyc": "^14.1.1", 45 | "rimraf": "^2.6.3", 46 | "ts-node": "^8.3.0", 47 | "tslint": "^5.17.0", 48 | "typescript": "^3.5.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as chaiAsPromised from "chai-as-promised"; 3 | chai.use(chaiAsPromised); 4 | const { expect } = chai; 5 | 6 | describe("getGoogleApiClient", () => { 7 | it("should add some tests", () => { 8 | expect(1).to.be.equal(1); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --exit 3 | --reporter spec 4 | --timeout 1000 5 | test/**/*.ts -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2015", 5 | "es6", 6 | "dom" 7 | ], 8 | "module": "commonjs", 9 | "target": "es5", 10 | "noImplicitAny": false, 11 | "moduleResolution": "node", 12 | "emitDecoratorMetadata": true, 13 | "noUnusedLocals": false, 14 | "sourceMap": true, 15 | "declaration": true, 16 | "rootDir": ".", 17 | "outDir": "dist", 18 | "allowSyntheticDefaultImports": true, 19 | "removeComments": true, 20 | "experimentalDecorators": true, 21 | "types": [ 22 | "node", 23 | "mocha" 24 | ] 25 | }, 26 | "include": [ 27 | "index.ts", 28 | "package.json", 29 | "src/**/*", 30 | "spec/**/*" 31 | ] 32 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": {}, 5 | "rules": { 6 | "object-literal-sort-keys": false, 7 | "no-console": false, 8 | "max-line-length": false 9 | }, 10 | "rulesDirectory": [] 11 | } 12 | --------------------------------------------------------------------------------