k2hr3-api
Version:
K2HR3 REST API is K2hdkc based Resource and Roles and policy Rules
1,001 lines (1,000 loc) • 42.5 kB
JavaScript
"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
*/