Skip to content

Commit

Permalink
feat(k8s): make it work with nginx gateway config on k8s
Browse files Browse the repository at this point in the history
  • Loading branch information
tadayosi committed Dec 16, 2021
1 parent eb104c2 commit 483341b
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 33 deletions.
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ RUN curl -sO http://nginx.org/keys/nginx_signing.key && \
# - modify perms for non-root runtime
RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
ln -sf /dev/stderr /var/log/nginx/error.log && \
sed -i 's/\/var\/run\/nginx.pid/\/var\/cache\/nginx\/nginx.pid/g' /etc/nginx/nginx.conf && \
sed -i 's|/var/run/nginx.pid|/var/cache/nginx/nginx.pid|g' /etc/nginx/nginx.conf && \
sed -i -e '/user/!b' -e '/nginx/!b' -e '/nginx/d' /etc/nginx/nginx.conf && \
echo -e "load_module modules/ngx_http_js_module.so;\n$(cat /etc/nginx/nginx.conf)" > /etc/nginx/nginx.conf && \
# Uncomment this line to output info log for nginx.js
#sed -i 's|/var/log/nginx/error.log warn|/var/log/nginx/error.log info|g' /etc/nginx/nginx.conf && \
rm -f /etc/nginx/conf.d/default.conf && \
chown -R 998 /var/cache/nginx /etc/nginx && \
chmod -R g=u /var/cache/nginx /etc/nginx
Expand All @@ -67,7 +69,7 @@ RUN touch config.js && \
mkdir -p /usr/share/nginx/html/integration/osconsole && \
ln -sf /config.js /usr/share/nginx/html/integration/osconsole/config.js

COPY docker/nginx.js docker/rbac.js docker/js-yaml.js /etc/nginx/conf.d/
COPY docker/nginx.js docker/rbac.js docker/js-yaml.js docker/jwt-decode.js /etc/nginx/conf.d/
COPY docker/nginx.conf docker/nginx-gateway.conf.template docker/osconsole/config.sh docker/nginx.sh docker/ACL.yaml /

COPY --from=builder /hawtio-online/docker/site /usr/share/nginx/html/
Expand Down
2 changes: 2 additions & 0 deletions deploy/bases/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ spec:
value: namespace
- name: HAWTIO_ONLINE_AUTH
value: form
- name: HAWTIO_ONLINE_RBAC_ACL
value: /etc/hawtio/rbac/ACL.yaml
- name: HAWTIO_ONLINE_NAMESPACE
valueFrom:
fieldRef:
Expand Down
13 changes: 0 additions & 13 deletions docker/.dockerignore

This file was deleted.

105 changes: 105 additions & 0 deletions docker/jwt-decode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
export default function (token, options) { 'use strict';

/**
* The code was extracted from:
* https://github.com/davidchambers/Base64.js
*/

var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

function InvalidCharacterError(message) {
this.message = message;
}

InvalidCharacterError.prototype = new Error();
InvalidCharacterError.prototype.name = "InvalidCharacterError";

function polyfill(input) {
var str = String(input).replace(/=+$/, "");
if (str.length % 4 == 1) {
throw new InvalidCharacterError(
"'atob' failed: The string to be decoded is not correctly encoded."
);
}
for (
// initialize result and counters
var bc = 0, bs, buffer, idx = 0, output = "";
// get next character
(buffer = str.charAt(idx++));
// character found in table? initialize bit storage and add its ascii value;
~buffer &&
((bs = bc % 4 ? bs * 64 + buffer : buffer),
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ?
(output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)))) :
0
) {
// try to find character in table (0-63, not found => -1)
buffer = chars.indexOf(buffer);
}
return output;
}

var atob = (typeof window !== "undefined" &&
window.atob &&
window.atob.bind(window)) ||
polyfill;

function b64DecodeUnicode(str) {
return decodeURIComponent(
atob(str).replace(/(.)/g, function(m, p) {
var code = p.charCodeAt(0).toString(16).toUpperCase();
if (code.length < 2) {
code = "0" + code;
}
return "%" + code;
})
);
}

function base64_url_decode(str) {
var output = str.replace(/-/g, "+").replace(/_/g, "/");
switch (output.length % 4) {
case 0:
break;
case 2:
output += "==";
break;
case 3:
output += "=";
break;
default:
throw "Illegal base64url string!";
}

try {
return b64DecodeUnicode(output);
} catch (err) {
return atob(output);
}
}

function InvalidTokenError(message) {
this.message = message;
}

InvalidTokenError.prototype = new Error();
InvalidTokenError.prototype.name = "InvalidTokenError";

function jwtDecode(token, options) {
if (typeof token !== "string") {
throw new InvalidTokenError("Invalid token specified");
}

options = options || {};
var pos = options.header === true ? 0 : 1;
try {
return JSON.parse(base64_url_decode(token.split(".")[pos]));
} catch (e) {
throw new InvalidTokenError("Invalid token specified: " + e.message);
}
}

return jwtDecode(token, options);
};
9 changes: 6 additions & 3 deletions docker/nginx-gateway.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ server {
client_body_buffer_size ${NGINX_CLIENT_BODY_BUFFER_SIZE};
proxy_buffers ${NGINX_PROXY_BUFFERS};

# For debugging location rewrite
#rewrite_log on;

if ($new) {
rewrite ^ $new redirect;
}
Expand Down Expand Up @@ -62,11 +65,11 @@ server {
js_content gateway.proxyJolokiaAgent;
}

# Self-LocalSubjectSccessReview requests cache
# Self-LocalSubjectAccessReview requests cache
location /authorization {
internal;
proxy_pass https://kubernetes.default/;
rewrite /authorization/(.*) /apis/authorization.openshift.io/v1/$1 break;
rewrite /authorization/([^/]+)/(.*) /apis/$1/v1/$2 break;
proxy_set_header Content-Type application/json;
proxy_pass_request_headers on;
proxy_pass_request_body on;
Expand All @@ -85,7 +88,7 @@ server {
location /authorization2 {
internal;
proxy_pass https://kubernetes.default/;
rewrite /authorization2/(.*) /apis/authorization.openshift.io/v1/$1 break;
rewrite /authorization2/([^/]+)/(.*) /apis/$1/v1/$2 break;
proxy_set_header Content-Type application/json;
proxy_pass_request_headers on;
proxy_pass_request_body on;
Expand Down
86 changes: 71 additions & 15 deletions docker/nginx.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
// https://github.com/xeioex/njs-examples

import RBAC from 'rbac.js';
import jwt_decode from 'jwt-decode.js';

var isRbacEnabled = typeof process.env['HAWTIO_ONLINE_RBAC_ACL'] !== 'undefined';
var useForm = process.env['HAWTIO_ONLINE_AUTH'] === 'form';

export default { proxyJolokiaAgent };

Expand All @@ -24,6 +26,8 @@ function proxyJolokiaAgent(req) {
var path = parts[5];

function response(res) {
req.log(`response: status=${res.status}`);

for (var header in res.headersOut) {
req.headersOut[header] = res.headersOut[header];
}
Expand All @@ -40,23 +44,65 @@ function proxyJolokiaAgent(req) {
});
}

function getSubjectFromJwt() {
var authz = req.headersIn['Authorization'];
if (!authz) {
req.error('Authorization header not found in request');
return '';
}
var token = authz.split(' ')[1];
var payload = jwt_decode(token);
return payload.sub;
}

function selfLocalSubjectAccessReview(verb) {
// Work-around same-location sub-requests caching issue
return req.subrequest(`/authorization${verb === 'get' ? '2' : ''}/namespaces/${namespace}/localsubjectaccessreviews`, {
method: 'POST',
body: JSON.stringify({
var api;
var body;
// When form is used, don't rely on OpenShift-specific LocalSubjectAccessReview
if (useForm) {
api = "authorization.k8s.io";
body = {
kind: 'LocalSubjectAccessReview',
apiVersion: 'authorization.k8s.io/v1',
metadata: {
namespace: namespace,
},
spec: {
user: getSubjectFromJwt(),
resourceAttributes: {
verb: verb,
resource: 'pods',
name: pod,
namespace: namespace,
}
}
};
} else {
api = "authorization.openshift.io";
body = {
kind: 'LocalSubjectAccessReview',
apiVersion: 'authorization.openshift.io/v1',
namespace: namespace,
verb: verb,
resource: 'pods',
name: pod,
}),
};
}
var json = JSON.stringify(body);
req.log(`selfLocalSubjectAccessReview(${verb}): ${api} - ${json}`);

// Work-around same-location sub-requests caching issue
var suffix = verb === 'get' ? '2' : '';
return req.subrequest(`/authorization${suffix}/${api}/namespaces/${namespace}/localsubjectaccessreviews`, {
method: 'POST',
body: json,
});
}

function getPodIP() {
return req.subrequest(`/podIP/${namespace}/${pod}`, { method: 'GET' }).then(res => {
req.log(`getPodIP(${namespace}/${pod}): status=${res.status}`);

if (res.status !== 200) {
return Promise.reject(res);
}
Expand All @@ -83,49 +129,59 @@ function proxyJolokiaAgent(req) {
// hosting the Jolokia endpoint is authorized
return selfLocalSubjectAccessReview('update')
.then(res => {
req.log(`proxyJolokiaAgentWithoutRbac(update): status=${res.status}`);

if (res.status !== 201) {
return Promise.reject(res);
}
var sar = JSON.parse(res.responseBody);
if (!sar.allowed) {
var allowed = useForm ? sar.status.allowed : sar.allowed;
if (!allowed) {
return reject(403, JSON.stringify(sar));
}
return getPodIP().then(podIP => {
req.log(`proxyJolokiaAgentWithoutRbac(podIP): podIP=${podIP}`);
return callJolokiaAgent(podIP, req.requestBody);
});
});
}

function proxyJolokiaAgentWithRbac() {
return selfLocalSubjectAccessReview('update')
.then(function (res) {
.then(res => {
req.log(`proxyJolokiaAgentWithRbac(update): status=${res.status}`);

if (res.status !== 201) {
return Promise.reject(res);
}
var sar = JSON.parse(res.responseBody);
if (sar.allowed) {
var allowed = useForm ? sar.status.allowed : sar.allowed;
if (allowed) {
// map the `update` verb to the `admin` role
return 'admin';
}
return selfLocalSubjectAccessReview('get')
.then(function (res) {
.then(res => {
req.log(`proxyJolokiaAgentWithRbac(get): status=${res.status}`);

if (res.status !== 201) {
return Promise.reject(res);
}
sar = JSON.parse(res.responseBody);
if (sar.allowed) {
allowed = useForm ? sar.status.allowed : sar.allowed;
if (allowed) {
// map the `get` verb to the `viewer` role
return 'viewer';
}
return reject(403, JSON.stringify(sar));
})
});
})
.then(function (role) {
.then(role => {
var request = JSON.parse(req.requestBody);
var requireMBeanDefinition;
if (Array.isArray(request)) {
requireMBeanDefinition = request.find(r => RBAC.isCanInvokeRequest(r));
return getPodIP().then(function (podIP) {
return getPodIP().then(podIP => {
return (requireMBeanDefinition ? listMBeans(podIP) : Promise.resolve()).then(beans => {
var rbac = request.map(r => RBAC.check(r, role));
var intercept = request.filter((_, i) => rbac[i].allowed).map(r => RBAC.intercept(r, role, beans));
Expand Down Expand Up @@ -163,7 +219,7 @@ function proxyJolokiaAgent(req) {
response.headersOut['Content-Length'] = response.responseBody.length;
return response;
});
})
});
});
} else {
requireMBeanDefinition = RBAC.isCanInvokeRequest(request);
Expand All @@ -178,7 +234,7 @@ function proxyJolokiaAgent(req) {
return Promise.resolve({ status: rbac.response.status, responseBody: JSON.stringify(rbac.response) });
}
return callJolokiaAgent(podIP, req.requestBody);
})
});
});
}
});
Expand Down

0 comments on commit 483341b

Please sign in to comment.