UNPKG

k2hr3-api

Version:

K2HR3 REST API is K2hdkc based Resource and Roles and policy Rules

1,001 lines (1,000 loc) 42.5 kB
"use strict"; /* * K2HR3 REST API * * Copyright 2017 Yahoo Japan Corporation. * * K2HR3 is K2hdkc based Resource and Roles and policy Rules, gathers * common management information for the cloud. * K2HR3 can dynamically manage information as "who", "what", "operate". * These are stored as roles, resources, policies in K2hdkc, and the * client system can dynamically read and modify these information. * * For the full copyright and license information, please view * the license file that was distributed with this source code. * * AUTHOR: Hirotaka Wakabayashi * CREATE: Fri, Aug 20 2021 * REVISION: * */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.k8soidc = void 0; //------------------------------------------------------------------------ // Usage //------------------------------------------------------------------------ // To enable this module, make the following settings in the K2HR3 API // configuration file(ex, production.json/local.json/etc). // // { // 'keystone': { // 'type': 'k8soidc' // } // } // // Set the value of the 'keystone'->'type' object to 'k8soidc'. // // Next, this module requires its own information, so the following // settings in configuration file are required. // // { // 'k8soidc': { // 'audience': '<client id for open id connect>', // 'issuer': '<issue url for open id connect>', // 'usernamekey': '<user name key name in token>', // 'k8sapi_url': '<kubernetes api url>', // 'k8s_ca_path': '<CA cert file path for kubernetes api url>', // 'k8s_sa_token': '<Service account token for kubernetes>' // 'unscopedtoken_exp':'<Expire limit for unscoped Token created from oidc>' // } // } // // Set the 'k8soidc' object as above. This object should contain the // following keys(objects). The contents of each setting are explained. // // [audience] // Set the client id for Open id connect. This key and value are // required. // [issuer] // Set the issuer URL of Open id connect. This key and value are // required. // [usernamekey] // Specify the key name that is the Username set in the Token of // Open id connect. If there is no key representing Username in // Token, it can be omitted. If omitted, the value of the 'sub' // key is treated as the Username. // [k8sapi_url] // Specify the URL of the Kubernetes API. This module accesses // the Kubernetes API to get the list of Kubernetes Namespaces. // For example, that is 'https://kubernetes.default.svc'. This key // and value are required. // [k8s_ca_path] // Specify the path of the CA certificate to access the Kubernetes // API. If you're running the K2HR3 API inside a Kubernetes pod, // it's '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'. // This key and value are required. // [k8s_sa_token] // Specify the Token of the Service Account to access the Kubernetes // API. If you're running the K2HR3 API inside a Kubernetes pod, // it's '/var/run/secrets/kubernetes.io/serviceaccount/token'. // This key and value are required. // [unscopedtoken_exp] // Specifies the expiration date of the Unscoped token created by // OIDC. This value is specified in seconds(s). // If this value does not exist or is less than or equal to 0, // the default value will be used. The default value is the same // as the OIDC token expiration date. // //------------------------------------------------------------------------ const k2hr3apiutil_1 = __importDefault(require("./k2hr3apiutil")); const k2hr3dkc_1 = __importDefault(require("./k2hr3dkc")); const dbglogging_1 = __importDefault(require("./dbglogging")); const fs = __importStar(require("fs")); const https = __importStar(require("https")); const k2hr3keys_1 = require("./k2hr3keys"); const client_node_1 = require("@kubernetes/client-node"); const jose_1 = require("jose"); const { decode } = jose_1.base64url; const k2hr3config_1 = require("./k2hr3config"); const apiConf = new k2hr3config_1.r3ApiConfig(); //--------------------------------------------------------- // Variables and Initializer //--------------------------------------------------------- // // Const Variables // const K8S_PUBLISHER_NAME = 'K8SOIDC'; const K8S_REGION_NAME = 'K8sCluster'; const OIDC_JWKS_URI_KEYNAME = 'jwks_uri'; // // Global variables from configuration file // let oidc_audience = null; let oidc_issuer = null; let oidc_username = null; let k8s_api_url = null; let k8s_ca_cert = null; let k2hr3_k8s_sa_token = null; let unscopedtoken_exp = 0; // Expire limit for unscoped Token created from oidc(default is 0 means as same as oidc limit) (() => { const tmp_oidc_config = apiConf.getOtherObject('k8soidc'); if (k2hr3apiutil_1.default.isPlainObject(tmp_oidc_config)) { oidc_audience = k2hr3apiutil_1.default.isString(tmp_oidc_config.audience) ? tmp_oidc_config.audience : null; oidc_issuer = k2hr3apiutil_1.default.isString(tmp_oidc_config.issuer) ? tmp_oidc_config.issuer : null; oidc_username = k2hr3apiutil_1.default.isString(tmp_oidc_config.usernamekey) ? tmp_oidc_config.usernamekey : null; k8s_api_url = k2hr3apiutil_1.default.isString(tmp_oidc_config.k8sapi_url) ? tmp_oidc_config.k8sapi_url : null; k8s_ca_cert = k2hr3apiutil_1.default.isString(tmp_oidc_config.k8s_ca_path) ? tmp_oidc_config.k8s_ca_path : null; try { k2hr3_k8s_sa_token = fs.readFileSync(k2hr3apiutil_1.default.getSafeString(tmp_oidc_config.k8s_sa_token), 'utf8'); } catch { k2hr3_k8s_sa_token = null; } // unscopedtoken_exp must be number if (k2hr3apiutil_1.default.isSafeNumber(tmp_oidc_config.unscopedtoken_exp) && 0 < tmp_oidc_config.unscopedtoken_exp) { unscopedtoken_exp = tmp_oidc_config.unscopedtoken_exp; } } })(); //--------------------------------------------------------- // User Token for k8s oidc //--------------------------------------------------------- // // user : user name which is verified authentication // user_id : user id which is verified authentication // expire_limit : specify expire second(default 24H = 24 * 60 * 60 sec), and allow empty // // result : { // result: true/false // message: null or error message string // token: undefined(error) or user token string // expire_at: expire date(UTC ISO 8601) // token_seed: JSON token seed data // userid: user id // } // // [NOTE] // user token seed value is following // { // publisher: "K8SOIDC" // userexid: "user extra id(a part of seed uuid4)" // date: "UTC ISO 8601 time at create" // expire: "UTC ISO 8601 time at expire" // creator: "User full yrn" // base: "32byte hex string" // user: "user name" // ip: always null // hostname: always null // port: always 0 // cuk: always null // extra: always null // tenant: if scoped token, this is "tenant name". if not, this is null // } // const rawCreateUserTokenByK8sUser = (user, user_id, tenant, expire_limit) => { const resobj = { result: true, message: null }; if (!k2hr3apiutil_1.default.isSafeString(user)) { // allow another parameter is null resobj.result = false; resobj.message = 'parameter is wrong : user=' + JSON.stringify(user); dbglogging_1.default.elog(resobj.message); return resobj; } const _user = user.toLowerCase(); if (!k2hr3apiutil_1.default.isString(user_id) || !k2hr3apiutil_1.default.isSafeStrUuid4(user_id)) { // user_id is uuid4 resobj.result = false; resobj.message = 'parameter is wrong : user_id(must be uuid4)=' + JSON.stringify(user_id); dbglogging_1.default.elog(resobj.message); return resobj; } const _user_id = user_id; let _tenant = null; if (k2hr3apiutil_1.default.isSafeString(tenant)) { _tenant = tenant; } let _expire_limit = 24 * 60 * 60; // default 24H if (0 < unscopedtoken_exp) { _expire_limit = unscopedtoken_exp; // override expire limit by config } else { if (k2hr3apiutil_1.default.isSafeNumber(expire_limit) && 0 < expire_limit) { _expire_limit = expire_limit; } } const dkcobj = k2hr3dkc_1.default.getK2hdkc(true, false); // use permanent object(need to clean) const keys = (0, k2hr3keys_1.getK2hr3Keys)(_user, null); if (!k2hr3apiutil_1.default.isSafeEntity(dkcobj)) { resobj.result = false; resobj.message = 'Not initialize yet.'; dbglogging_1.default.elog(resobj.message); return resobj; } const user_id_uuid4 = _user_id; // user id must be UUID4 const user_ex_id = k2hr3apiutil_1.default.getStrUuid4(); // set seed(uuid4) // make token seed value const now_unixtime = k2hr3apiutil_1.default.getUnixtime(); // user token and yrn key let user_token = ''; let user_token_base = ''; // create key for (let is_loop = true; is_loop;) { // make user token const token_elements = k2hr3apiutil_1.default.makeStringToken256(user_ex_id, user_id_uuid4); if (!k2hr3apiutil_1.default.isSafeEntity(token_elements)) { resobj.result = false; resobj.message = 'could not make token from ' + JSON.stringify(user_ex_id) + ' and ' + JSON.stringify(_user_id); dbglogging_1.default.elog(resobj.message); dkcobj.clean(); return resobj; } user_token = token_elements.str_token; user_token_base = token_elements.str_base; // token base // user token key const token_user_key = keys.TOKEN_USER_TOP_KEY + '/' + user_token; // "yrn:yahoo::::token:user/<user token>" // get user token for existing check const value = dkcobj.getValue(token_user_key, null, true, null); if (!k2hr3apiutil_1.default.isSafeEntity(value)) { // succeed uniq token break; } dbglogging_1.default.dlog('conflict user token(' + user_token + ') which already is used, so remake token for uniq.'); } const token_seed = { publisher: K8S_PUBLISHER_NAME, // "K8SOIDC" userexid: user_ex_id, // seed(uuid4) date: (new Date(now_unixtime * 1000)).toISOString(), // now date(UTC ISO 8601) expire: (new Date((now_unixtime + _expire_limit) * 1000)).toISOString(), // expire date(UTC ISO 8601) creator: keys.USER_KEY, // "yrn:yahoo::::user:<user>" base: user_token_base, // token bease user: _user, // user(creator) ip: null, // ip(creator) hostname: null, // hostname(creator) port: 0, // port(creator) cuk: null, // cuk(creator) extra: null, // extra(creator) tenant: _tenant // tenant(if scope, not null) }; // Add user token/expire/seed into result object. resobj.token = user_token; resobj.expire_at = token_seed.expire; resobj.token_seed = JSON.stringify(token_seed); resobj.userid = _user_id; dkcobj.clean(); return resobj; }; //--------------------------------------------------------- // Verify User Token Publisher For k8s oidc //--------------------------------------------------------- // // token_seed : token seed data // // result : { // result: true/false // message: null or error message string // } // const rawWrapVerifyUserTokenPublisherForK8s = (token_seed) => { const resobj = { result: true, message: null }; // parse seed if (!k2hr3apiutil_1.default.checkSimpleJSON(token_seed)) { resobj.result = false; resobj.message = 'token_seed(not printable) is not safe entity.'; dbglogging_1.default.elog(resobj.message); return resobj; } const tmpseed = k2hr3apiutil_1.default.parseJSON(token_seed); if (!k2hr3apiutil_1.default.isValTypeTokenSeed(tmpseed)) { resobj.result = false; resobj.message = 'token_seed(not printable) is not safe entity.'; dbglogging_1.default.elog(resobj.message); return resobj; } const seed = tmpseed; if (!k2hr3apiutil_1.default.isSafeString(seed.publisher) || (seed.publisher != K8S_PUBLISHER_NAME)) // publisher must be 'K8SOIDC' { resobj.result = false; resobj.message = 'token_seed(not printable) is not safe entity.'; dbglogging_1.default.elog(resobj.message); return resobj; } return resobj; }; //--------------------------------------------------------- // Verify User Token (OIDC Token) //--------------------------------------------------------- // // dkcobj_permanent : dkcobj object // user : target user name for token // tenant : target tenant name for token(if token is scoped) // token : check token // token_seed : token seed data // // result : { // result: true/false // message: null or error message string // } // const rawVerifyUserTokenByK8sUser = (dkcobj_permanent, user, tenant, token, token_seed) => { const resobj = { result: true, message: null }; if (!k2hr3apiutil_1.default.isSafeString(token) || !k2hr3apiutil_1.default.isSafeString(token_seed) || !k2hr3apiutil_1.default.isSafeString(user)) { resobj.result = false; resobj.message = 'some parameters are wrong : token=' + JSON.stringify(token) + ', token_seed=<not printable>, user=' + JSON.stringify(user); dbglogging_1.default.elog(resobj.message); return resobj; } // parse seed if (!k2hr3apiutil_1.default.checkSimpleJSON(token_seed)) { resobj.result = false; resobj.message = 'token_seed(not printable) is not safe entity.'; dbglogging_1.default.elog(resobj.message); return resobj; } const tmpseed = k2hr3apiutil_1.default.parseJSON(token_seed); if (!k2hr3apiutil_1.default.isValTypeTokenSeed(tmpseed)) { resobj.result = false; resobj.message = 'token_seed(not printable) is not safe entity.'; dbglogging_1.default.elog(resobj.message); return resobj; } // check all seed values const seed = tmpseed; if (!k2hr3apiutil_1.default.isSafeString(seed.publisher) || (seed.publisher != K8S_PUBLISHER_NAME) || // publisher must be 'K8SOIDC' !k2hr3apiutil_1.default.isSafeString(seed.userexid) || !k2hr3apiutil_1.default.isSafeString(seed.date) || !k2hr3apiutil_1.default.isSafeString(seed.expire) || !k2hr3apiutil_1.default.isSafeString(seed.creator) || !k2hr3apiutil_1.default.isSafeString(seed.base) || !k2hr3apiutil_1.default.isSafeString(seed.user) || !k2hr3apiutil_1.default.compareCaseString(seed.user, user)) { resobj.result = false; resobj.message = 'token_seed(not printable) is not safe entity.'; dbglogging_1.default.elog(resobj.message); return resobj; } // check expire if (k2hr3apiutil_1.default.isExpired(seed.expire)) { resobj.result = false; resobj.message = 'token is expired by expire date(' + JSON.stringify(seed.expire) + ') in token_seed.'; dbglogging_1.default.elog(resobj.message); return resobj; } // check tenant name(if tenant is specified, seed must have same tenant name) if (k2hr3apiutil_1.default.isSafeString(seed.tenant) !== k2hr3apiutil_1.default.isSafeString(tenant) || (k2hr3apiutil_1.default.isSafeString(seed.tenant) && !k2hr3apiutil_1.default.compareCaseString(seed.tenant, tenant))) { resobj.result = false; resobj.message = 'token_seed(not printable) is (un)scoped, but tenant name is (not) specified. Then unmatched.'; dbglogging_1.default.elog(resobj.message); return resobj; } // k2hdkc const keys = (0, k2hr3keys_1.getK2hr3Keys)(seed.user, seed.tenant); let dkcobj = dkcobj_permanent; if (!k2hr3apiutil_1.default.isSafeEntity(dkcobj)) { dkcobj = k2hr3dkc_1.default.getK2hdkc(true, false); // use permanent object(need to clean) if (!k2hr3apiutil_1.default.isSafeEntity(dkcobj)) { resobj.result = false; resobj.message = 'Not initialize yet.'; dbglogging_1.default.elog(resobj.message); return resobj; } } // get user id const userid = dkcobj.getValue(keys.USER_ID_KEY, null, true, null); // get user id from "yrn:yahoo::::user:<user>:id" if (!k2hr3apiutil_1.default.isSafeString(userid)) { resobj.result = false; resobj.message = 'could not get user id for user(' + seed.user + ').'; dbglogging_1.default.elog(resobj.message); return resobj; } // make verify token const token_elements = k2hr3apiutil_1.default.makeStringToken256(seed.userexid, userid, seed.base); if (!k2hr3apiutil_1.default.isSafeEntity(token_elements)) { resobj.result = false; resobj.message = 'could not make verify token from ' + JSON.stringify(seed.userexid) + ' and ' + JSON.stringify(userid) + ' and ' + JSON.stringify(seed.base); dbglogging_1.default.elog(resobj.message); return resobj; } if (token !== token_elements.str_token) { resobj.result = false; resobj.message = 'token(' + token + ') verify is failure, verify token is ' + token_elements.str_token + '.'; dbglogging_1.default.elog(resobj.message); return resobj; } return resobj; }; //--------------------------------------------------------- // Get User/Tenant information by User Token //--------------------------------------------------------- // // Result: { // result: true/false // message: null or error message string // user: user name // userid: user id // tenant: if token is scoped token, this value is set tenant name. // } // const rawGetUserTenantInfoByUserToken = (token) => { const resobj = { result: true, message: null }; if (!k2hr3apiutil_1.default.isSafeString(token)) { resobj.result = false; resobj.message = 'parameter is wrong : token=' + JSON.stringify(token); dbglogging_1.default.elog(resobj.message); return resobj; } const dkcobj = k2hr3dkc_1.default.getK2hdkc(true, false); // use permanent object(need to clean) let keys = (0, k2hr3keys_1.getK2hr3Keys)(); if (!k2hr3apiutil_1.default.isSafeEntity(dkcobj)) { resobj.result = false; resobj.message = 'Not initialize yet.'; dbglogging_1.default.elog(resobj.message); return resobj; } // get token key under user key const token_value_key = keys.TOKEN_USER_TOP_KEY + '/' + token; // "yrn:yahoo::::token:user/<token>" const user_token_key = dkcobj.getValue(token_value_key, null, true, null); // "yrn:yahoo::::user:<user>:tenant/{<tenant>}/token/<token>" if (!k2hr3apiutil_1.default.isSafeString(user_token_key)) { resobj.result = false; resobj.message = 'token key(' + token_value_key + ') for token(' + token + ') is not existed.'; dbglogging_1.default.elog(resobj.message); dkcobj.clean(); return resobj; } // get user name and tenant name from token key yrn path const pattern = new RegExp('^' + keys.MATCH_ANY_USER_TOKEN); // regex = /^yrn:yahoo::::user:(.*):tenant\/(.*)\/token\/(.*)/ const matches = user_token_key.match(pattern); // reverse to user/tenant names if (!k2hr3apiutil_1.default.isNotEmptyArray(matches) || matches.length < 4 || '' === k2hr3apiutil_1.default.getSafeString(matches[1])) { resobj.result = false; resobj.message = 'token key(' + token_value_key + ') for token(' + token + ') has wrong format value(' + user_token_key + ')'; dbglogging_1.default.elog(resobj.message); dkcobj.clean(); return resobj; } const token_user = k2hr3apiutil_1.default.getSafeString(matches[1]); const token_tenant = k2hr3apiutil_1.default.isString(matches[2]) ? matches[2] : null; // get token seed const user_token_seed_key = user_token_key + '/' + keys.SEED_KW; // "yrn:yahoo::::user:<user>:tenant/{<tenant>}/token/<token>/seed" const token_seed = dkcobj.getValue(user_token_seed_key, null, true, null); if (!k2hr3apiutil_1.default.isSafeString(token_seed)) { resobj.result = false; resobj.message = 'token key(' + token_value_key + ') for token(' + token + ') does not have token seed data.'; dbglogging_1.default.elog(resobj.message); dkcobj.clean(); return resobj; } // verify token const vres = rawVerifyUserTokenByK8sUser(dkcobj, token_user, token_tenant, token, token_seed); if (!vres.result) { resobj.result = false; resobj.message = 'failed to verify token(' + token + ') with seed by ' + vres.message; dbglogging_1.default.elog(resobj.message); dkcobj.clean(); return resobj; } // get user id keys = (0, k2hr3keys_1.getK2hr3Keys)(token_user, null); // remake keys const userid = dkcobj.getValue(keys.USER_ID_KEY, null, true, null); // get user id from "yrn:yahoo::::user:<user>:id" if (!k2hr3apiutil_1.default.isSafeString(userid)) { resobj.result = false; resobj.message = 'could not get user id for user(' + token_user + ').'; dbglogging_1.default.elog(resobj.message); dkcobj.clean(); return resobj; } dkcobj.clean(); // make result resobj.user = token_user; resobj.userid = userid; resobj.tenant = token_tenant; return resobj; }; //--------------------------------------------------------- // Verify (Un)scoped Token //--------------------------------------------------------- // // token : unscoped/scoped token // const rawVerifyUnscopedToken = (token) => { if (!k2hr3apiutil_1.default.isSafeString(token)) { dbglogging_1.default.elog('token(' + JSON.stringify(token) + ') parameter is wrong.'); return false; } const dkcobj = k2hr3dkc_1.default.getK2hdkc(true, false); // use permanent object(need to clean) const keys = (0, k2hr3keys_1.getK2hr3Keys)(); if (!k2hr3apiutil_1.default.isSafeEntity(dkcobj)) { dbglogging_1.default.elog('K2hdkc client is not initialized yet.'); return false; } // get token key under user key const token_value_key = keys.TOKEN_USER_TOP_KEY + '/' + token; // "yrn:yahoo::::token:user/<token>" const user_token_key = dkcobj.getValue(token_value_key, null, true, null); // "yrn:yahoo::::user:<user>:tenant/{<tenant>}/token/<token>" if (!k2hr3apiutil_1.default.isSafeString(user_token_key)) { dbglogging_1.default.elog('token key(' + token_value_key + ') for token(' + token + ') is not existed.'); dkcobj.clean(); return false; } // get user name and tenant name from token key yrn path const pattern = new RegExp('^' + keys.MATCH_ANY_USER_TOKEN); // regex = /^yrn:yahoo::::user:(.*):tenant\/(.*)\/token\/(.*)/ const matches = user_token_key.match(pattern); // reverse to user/tenant names if (!k2hr3apiutil_1.default.isNotEmptyArray(matches) || matches.length < 4 || '' === k2hr3apiutil_1.default.getSafeString(matches[1])) { dbglogging_1.default.elog('token key(' + token_value_key + ') for token(' + token + ') has wrong format value(' + user_token_key + ')'); dkcobj.clean(); return false; } const token_user = k2hr3apiutil_1.default.getSafeString(matches[1]); let token_tenant = k2hr3apiutil_1.default.getSafeString(matches[2]); if ('' === token_tenant) { token_tenant = null; } // get token seed const user_token_seed_key = user_token_key + '/' + keys.SEED_KW; // "yrn:yahoo::::user:<user>:tenant/{<tenant>}/token/<token>/seed" const token_seed = dkcobj.getValue(user_token_seed_key, null, true, null); if (!k2hr3apiutil_1.default.isSafeString(token_seed)) { dbglogging_1.default.elog('token key(' + token_value_key + ') for token(' + token + ') does not have token seed data.'); dkcobj.clean(); return false; } // verify token const vres = rawVerifyUserTokenByK8sUser(dkcobj, token_user, token_tenant, token, token_seed); if (!vres.result) { dbglogging_1.default.elog('failed to verify token(' + token + ') with seed by ' + vres.message); dkcobj.clean(); return false; } dkcobj.clean(); return true; }; //--------------------------------------------------------- // Get Scoped token from k8s user token //--------------------------------------------------------- // // callback(error, result): // result = { // user: user name // userid: user id // scoped: always true // token: token string // expire: expire string(UTC ISO 8601) // region: region string // token_seed: JSON token seed data // } // // [NOTE] // The token is allowed scoped token, but it must be same tenant token. // const rawGetUserScopedTokenK8s = (token, tenant, callback) => { if (!k2hr3apiutil_1.default.isSafeString(token) || !k2hr3apiutil_1.default.isSafeString(tenant)) { const error = new Error('some parameters are wrong : token=' + JSON.stringify(token) + ', tenant=' + JSON.stringify(tenant)); dbglogging_1.default.elog(error.message); callback(error, null); return; } // verify and get user/tenant information const token_info = rawGetUserTenantInfoByUserToken(token); if (!token_info.result) { const error = new Error('could not get any information from token(' + token + '), result : ' + token_info.message); dbglogging_1.default.elog(error.message); callback(error, null); return; } // check tenant name if (k2hr3apiutil_1.default.isSafeString(token_info.tenant) && token_info.tenant !== tenant) { const error = new Error('token(' + token + ') has scoped(' + token_info.tenant + '), but it is not as same as the request tenant(' + tenant + ').'); dbglogging_1.default.elog(error.message); callback(error, null); return; } // create scoped token const resobj = rawCreateUserTokenByK8sUser(k2hr3apiutil_1.default.getSafeString(token_info.user), k2hr3apiutil_1.default.getSafeString(token_info.userid), tenant); // not specify expire limit now(using default). if (!resobj.result) { const error = new Error('could not create user scoped token for uname(' + token_info.user + ')/user id(' + token_info.userid + ') for tenant(' + tenant + ').'); dbglogging_1.default.elog(error.message); callback(error, null); return; } // make result const result = { user: k2hr3apiutil_1.default.getSafeString(token_info.user), userid: k2hr3apiutil_1.default.getSafeString(token_info.userid), scoped: true, token: k2hr3apiutil_1.default.getSafeString(resobj.token), expire: k2hr3apiutil_1.default.isString(resobj.expire_at) ? resobj.expire_at : null, region: K8S_REGION_NAME, token_seed: k2hr3apiutil_1.default.getSafeString(resobj.token_seed), }; callback(null, result); }; //--------------------------------------------------------- // Get Unscoped token by oidc token //--------------------------------------------------------- // // callback(error, result): // result = { // user: user name User name in token: set user name if specified user name key name in config. if not specified, set user id // userid: user id User id in token: payload in token has 'sub' key, it is user id. // scoped: false (always false) // token: token string(id) OIDC Token // expire: expire string expire in token: payload in token has 'exp' key, it is expire unix time. // region: region string (always n/a) // token_seed: seed ({publisher: 'K8SOIDC'}) // } // // // Utility - Verify OIDC token and get user name // // token: oidc token // const rawVerifyTokenAndGetUsername = async (token) => { const jwtParam = { issuer: oidc_issuer ?? undefined, audience: oidc_audience ?? undefined }; const myPromise = (issuer_url, conf_key) => { return new Promise((resolve, reject) => { https.get(issuer_url + '/.well-known/openid-configuration', (res) => { if (res.statusCode !== 200) { res.resume(); reject('statusCode should be 200, not ' + String(res.statusCode)); } res.setEncoding('utf8'); let rawData = ''; res.on('data', (chunk) => { rawData += chunk; }); res.on('end', () => { const parsedData = k2hr3apiutil_1.default.parseJSON(rawData); if (k2hr3apiutil_1.default.isPlainObject(parsedData) && k2hr3apiutil_1.default.isString(parsedData[conf_key])) { resolve(parsedData[conf_key]); } else { const errorMsg = ('the ' + conf_key + ' key should exist, but no such a key'); dbglogging_1.default.elog(errorMsg); reject(errorMsg); } }); }).on('error', (err) => { reject(err); }); }); }; const asyncFunction = async () => { let oidc_jwks_uri; try { oidc_jwks_uri = await myPromise(k2hr3apiutil_1.default.getSafeString(oidc_issuer), OIDC_JWKS_URI_KEYNAME); if (!k2hr3apiutil_1.default.isSafeString(oidc_jwks_uri)) { const error = new Error('oidc_jwks_uri should be defined, but no oidc_jwks_uri.'); dbglogging_1.default.elog(error.message); throw error; } } catch (err) { if (err instanceof Error) { dbglogging_1.default.elog(err.message); } throw err; } const JWKS = (0, jose_1.createRemoteJWKSet)(new URL(oidc_jwks_uri)); let payload; try { const jwtResult = await (0, jose_1.jwtVerify)(token, JWKS, jwtParam); payload = jwtResult.payload; } catch (err) { if (err instanceof Error) { dbglogging_1.default.elog(err.message); } throw err; } let userName = null; if (k2hr3apiutil_1.default.isPlainObject(payload) && k2hr3apiutil_1.default.isSafeString(oidc_username) && k2hr3apiutil_1.default.isString(payload[oidc_username])) { userName = k2hr3apiutil_1.default.getSafeString(payload[oidc_username]); } else { if (k2hr3apiutil_1.default.isPlainObject(payload) && k2hr3apiutil_1.default.isSafeString(payload.sub)) { userName = payload.sub; } } if (!k2hr3apiutil_1.default.isSafeString(userName)) { const error = new Error('failed to verify token for getting user name.'); dbglogging_1.default.elog(error.message); throw error; } return userName; }; return asyncFunction(); }; const rawGetUserUnscopedTokenK8s = (token, callback) => { if (!k2hr3apiutil_1.default.isSafeString(token)) { const error = new Error('oidc token parameter is not string or empty.'); dbglogging_1.default.elog(error.message); callback(error, null); return; } // // Check the id_token. // // see. https://openid.net/specs/openid-connect-core-1_0.html#IDToken // const parts = token.split('.', 2); if (2 !== parts.length) { const error = new Error('oidc token must have two parts, but it has ' + parts.length + ' parts.'); dbglogging_1.default.elog(error.message); callback(error, null); return; } // // decode part[1] to payload // const payload = k2hr3apiutil_1.default.parseJSON(new TextDecoder().decode(decode(parts[1]))); if (!k2hr3apiutil_1.default.isPlainObject(payload)) { const error = new Error('could not decode json from the part[1] in oidc token.'); dbglogging_1.default.elog(error.message); callback(error, null); return; } // // payload must have 'sub' key for user id // let userid; if (k2hr3apiutil_1.default.isSafeString(payload.sub)) { userid = payload.sub; } else { const error = new Error('token payload should contain sub(userid), but not find sub(userid).'); dbglogging_1.default.elog(error.message); callback(error, null); return; } // // get user name from payload // let username; if (k2hr3apiutil_1.default.isSafeString(oidc_username)) { if (k2hr3apiutil_1.default.isSafeString(payload[oidc_username])) { username = k2hr3apiutil_1.default.getSafeString(payload[oidc_username]); } else { const error = new Error('token payload should contain user name(' + oidc_username + '), but not find it.'); dbglogging_1.default.elog(error.message); callback(error, null); return; } } else { // If user name key is not specified, user id will be used instead. username = userid; } // // verify token by JWT library and get user name // const _callback = callback; const lower_username = username.toLowerCase(); // to lower case rawVerifyTokenAndGetUsername(token).then((result) => { const verified_username = k2hr3apiutil_1.default.getSafeString(result); // // compare user name // if (!k2hr3apiutil_1.default.compareCaseString(lower_username, verified_username)) { const error = new Error('oidc token has ' + lower_username + ' username, but verified username(' + verified_username + ') is different.'); dbglogging_1.default.elog(error.message); _callback(error, null); return; } // core seed const user_id_uuid4 = k2hr3apiutil_1.default.cvtNumberStringToUuid4(userid, 10); // payload.sub is decimal string let expire_limit; if (k2hr3apiutil_1.default.isSafeNumber(payload['exp'])) { expire_limit = payload['exp'] - k2hr3apiutil_1.default.getUnixtime(); if (expire_limit <= 0) { expire_limit = 24 * 60 * 60; // default 24H } } else { expire_limit = 24 * 60 * 60; // default 24H } // create token const resobj = rawCreateUserTokenByK8sUser(lower_username, user_id_uuid4, null, expire_limit); if (!resobj.result) { const error = new Error('could not create user token for uname(' + lower_username + ') or something wrong result : ' + resobj.message); dbglogging_1.default.elog(error.message); _callback(error, null); return; } // make result const resultobj = { user: lower_username, userid: k2hr3apiutil_1.default.getSafeString(resobj.userid), scoped: false, token: k2hr3apiutil_1.default.getSafeString(resobj.token), expire: k2hr3apiutil_1.default.isSafeString(resobj.expire_at) ? resobj.expire_at : null, region: K8S_REGION_NAME, token_seed: k2hr3apiutil_1.default.getSafeString(resobj.token_seed) }; _callback(null, resultobj); }).catch((err) => { dbglogging_1.default.elog(err.message); _callback(err, null); }); }; const hasSetDefaultAuthentication = (obj) => { return k2hr3apiutil_1.default.isPlainObject(obj) && k2hr3apiutil_1.default.isFunction(obj.setDefaultAuthentication); }; // // unscopedtoken: oidc token(Not use) // const rawGetUserTenantListK8s = (unscopedtoken, callback) => { if (!k2hr3apiutil_1.default.isSafeString(unscopedtoken)) { const error = new Error('unscopedtoken parameter is wrong.'); dbglogging_1.default.elog(error.message); callback(error, null); return; } // Verify unscoped token if (!rawVerifyUnscopedToken(unscopedtoken)) { const error = new Error('unscopedtoken is not safe, varidation is failed.'); dbglogging_1.default.elog(error.message); callback(error, null); return; } const _callback = callback; // // Get Namespaces by Service Access token // const cluster = { name: 'k2hr3-api-k8soidc-cluster', server: k2hr3apiutil_1.default.getSafeString(k8s_api_url), caFile: k2hr3apiutil_1.default.getSafeString(k8s_ca_cert), skipTLSVerify: false }; const user = { name: 'k2hr3-api-k8soidc-name', token: k2hr3apiutil_1.default.getSafeString(k2hr3_k8s_sa_token) // [NOTE] k2hr3_k8s_sa_token is global variable }; const kubeconfig = new client_node_1.KubeConfig(); kubeconfig.loadFromClusterAndUser(cluster, user); const k8sApi = kubeconfig.makeApiClient(client_node_1.CoreV1Api); if (hasSetDefaultAuthentication(k8sApi)) { k8sApi.setDefaultAuthentication({ applyToRequest: (opts) => { if (!k2hr3apiutil_1.default.isPlainObject(opts.headers)) { opts.headers = {}; } opts.headers.Authorization = 'Bearer ' + k2hr3_k8s_sa_token; // [NOTE] k2hr3_k8s_sa_token is global variable opts.rejectUnauthorized = false; } }); } else { const error = new Error('Not found CoreV1Api.setDefaultAuthentication function.'); dbglogging_1.default.elog(error.message); callback(error, null); return; } const k8s_ns = ['kube-node-lease', 'kube-public', 'kube-system', 'kubernetes-dashboard']; const resarr = []; k8sApi.listNamespace().then((response) => { if (k2hr3apiutil_1.default.isPlainObject(response) && k2hr3apiutil_1.default.isPlainObject(response.body) && k2hr3apiutil_1.default.isArray(response.body.items)) { for (let pos = 0; pos < response.body.items.length; ++pos) { // check body...items const tmpItem = response.body.items[pos]; if (!k2hr3apiutil_1.default.isPlainObject(tmpItem) || !k2hr3apiutil_1.default.isPlainObject(tmpItem.metadata) || !k2hr3apiutil_1.default.isSafeString(tmpItem.metadata.name)) { dbglogging_1.default.wlog('one of response for project(tenant) list is something wrong : ' + JSON.stringify(response.body)); continue; } // Is the k8s cluster namespace in kubernetes system namespaces? if (k2hr3apiutil_1.default.findStringInArray(k8s_ns, tmpItem.metadata.name)) { continue; } const tenant = { name: tmpItem.metadata.name, id: k2hr3apiutil_1.default.getSafeString(tmpItem.metadata.uid), description: tmpItem.metadata.name, display: tmpItem.metadata.name }; resarr.push(tenant); } } if (0 === resarr.length) { const error = new Error('no tenant exists'); dbglogging_1.default.elog(error.message); _callback(error, null); return; } _callback(null, resarr); }).catch((error) => { dbglogging_1.default.elog(error.message); _callback(error, null); }); }; //--------------------------------------------------------- // Exports //--------------------------------------------------------- exports.k8soidc = { // // uname: username // passwd: passwd // getUserUnscopedToken: (uname, passwd, callback) => { const error = new Error('getUserUnscopedToken is not implemented'); dbglogging_1.default.elog(error.message); callback(error, null); }, // // Get Unscoped Token // // token: OIDC token // getUserUnscopedTokenByToken: rawGetUserUnscopedTokenK8s, // // Get Scoped Token // // tenantid: not used // getUserScopedToken: rawGetUserScopedTokenK8s, // // Verify publisher type in seed // verifyUserTokenPublisher: rawWrapVerifyUserTokenPublisherForK8s, // // Verify token // verifyUserToken: (dkcobj_permanent, user, tenant, token, token_seed) => { return rawVerifyUserTokenByK8sUser(dkcobj_permanent, user, tenant, token, token_seed); }, // // Get tenant list // getUserTenantList: (unscopedtoken, userid, callback) => { rawGetUserTenantListK8s(unscopedtoken, callback); } }; // // Default // exports.default = exports.k8soidc; /* * Local variables: * tab-width: 4 * c-basic-offset: 4 * End: * vim600: noexpandtab sw=4 ts=4 fdm=marker * vim<600: noexpandtab sw=4 ts=4 */