diff --git a/unison/unison-applications-k8s/src/main/java/com/tremolosecurity/proxy/filters/K8sToken.java b/unison/unison-applications-k8s/src/main/java/com/tremolosecurity/proxy/filters/K8sToken.java new file mode 100644 index 00000000..45aca48a --- /dev/null +++ b/unison/unison-applications-k8s/src/main/java/com/tremolosecurity/proxy/filters/K8sToken.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright 2019 Tremolo Security, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package com.tremolosecurity.proxy.filters; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import com.tremolosecurity.proxy.auth.AuthController; +import com.tremolosecurity.proxy.auth.AuthInfo; +import com.tremolosecurity.proxy.filter.HttpFilter; +import com.tremolosecurity.proxy.filter.HttpFilterChain; +import com.tremolosecurity.proxy.filter.HttpFilterConfig; +import com.tremolosecurity.proxy.filter.HttpFilterRequest; +import com.tremolosecurity.proxy.filter.HttpFilterResponse; +import com.tremolosecurity.proxy.util.ProxyConstants; +import com.tremolosecurity.saml.Attribute; +import com.tremolosecurity.server.GlobalEntries; +import com.tremolosecurity.unison.openshiftv3.OpenShiftTarget; + +public class K8sToken implements HttpFilter { + + + String userNameAttribute; + String groupAttribute; + boolean useLdapGroups; + String targetName; + + + @Override + public void doFilter(HttpFilterRequest request, HttpFilterResponse response, HttpFilterChain chain) + throws Exception { + Iterator it = request.getHeaderNames(); + List toRemove = new ArrayList(); + while (it.hasNext()) { + String headerName = it.next(); + if (headerName.toLowerCase().startsWith("impersonate-") || headerName.equalsIgnoreCase("Authorization")) { + toRemove.add(headerName); + } + } + + for (String headerToRemove : toRemove) { + request.removeHeader(headerToRemove); + } + + request.removeHeader("Authorization"); + + + + + OpenShiftTarget target = (OpenShiftTarget) GlobalEntries.getGlobalEntries().getConfigManager().getProvisioningEngine().getTarget(this.targetName).getProvider(); + + String token = target.getAuthToken(); + + if (token != null) { + request.addHeader(new Attribute("Authorization",new StringBuilder().append("Bearer ").append(target.getAuthToken()).toString())); + } + + + + HashMap uriParams = (HashMap) request.getAttribute("TREMOLO_URI_PARAMS"); + uriParams.put("k8s_url", target.getUrl()); + + + chain.nextFilter(request, response, chain); + + + } + + @Override + public void filterResponseText(HttpFilterRequest request, HttpFilterResponse response, HttpFilterChain chain, + StringBuffer data) throws Exception { + // TODO Auto-generated method stub + + } + + @Override + public void filterResponseBinary(HttpFilterRequest request, HttpFilterResponse response, HttpFilterChain chain, + byte[] data, int length) throws Exception { + // TODO Auto-generated method stub + + } + + @Override + public void initFilter(HttpFilterConfig config) throws Exception { + this.targetName = config.getAttribute("targetName").getValues().get(0); + + + } + +} diff --git a/unison/unison-applications-openshift3/pom.xml b/unison/unison-applications-openshift3/pom.xml index 4cef9647..648ab25d 100644 --- a/unison/unison-applications-openshift3/pom.xml +++ b/unison/unison-applications-openshift3/pom.xml @@ -35,6 +35,12 @@ json-simple ${json-simple.version} + + + org.bitbucket.b_c + jose4j + ${jose4j.version} + diff --git a/unison/unison-applications-openshift3/src/main/java/com/tremolosecurity/unison/openshiftv3/OpenShiftTarget.java b/unison/unison-applications-openshift3/src/main/java/com/tremolosecurity/unison/openshiftv3/OpenShiftTarget.java index e60f3db9..a76b83cb 100644 --- a/unison/unison-applications-openshift3/src/main/java/com/tremolosecurity/unison/openshiftv3/OpenShiftTarget.java +++ b/unison/unison-applications-openshift3/src/main/java/com/tremolosecurity/unison/openshiftv3/OpenShiftTarget.java @@ -58,6 +58,14 @@ import org.cryptacular.EncodingException; import org.cryptacular.StreamException; import org.cryptacular.util.CertUtil; +import org.joda.time.DateTime; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.lang.JoseException; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; @@ -66,6 +74,8 @@ import com.google.gson.reflect.TypeToken; import com.tremolosecurity.certs.CertManager; import com.tremolosecurity.config.util.ConfigManager; +import com.tremolosecurity.config.xml.ApplicationType; +import com.tremolosecurity.config.xml.ParamType; import com.tremolosecurity.provisioning.core.ProvisioningException; import com.tremolosecurity.provisioning.core.User; import com.tremolosecurity.provisioning.core.UserStoreProvider; @@ -80,6 +90,16 @@ import com.tremolosecurity.unison.openshiftv3.model.groups.GroupItem; public class OpenShiftTarget implements UserStoreProviderWithAddGroup { + + public enum TokenType { + NONE, + STATIC, + LEGACY, + TOKENAPI, + OIDC + } + + private TokenType tokenType; static Logger logger = org.apache.logging.log4j.LogManager.getLogger(OpenShiftTarget.class.getName()); @@ -97,6 +117,19 @@ public class OpenShiftTarget implements UserStoreProviderWithAddGroup { private boolean localToken; + private String tokenPath; + private DateTime tokenExpires; + + private String oidcIdp; + private String oidcTrustName; + private String oidcIssuer; + private String oidcSub; + private String oidcCertName; + + private String oidcAudience; + + private String oidcIssuerHost; + @Override public void createUser(User user, Set attributes, Map request) throws ProvisioningException { @@ -449,9 +482,13 @@ public String callWS(String token, HttpCon con,String uri) throws IOException, C b.append(this.getUrl()).append(uri); HttpGet get = new HttpGet(b.toString()); - b.setLength(0); - b.append("Bearer ").append(token); - get.addHeader(new BasicHeader("Authorization","Bearer " + token)); + + if (token != null) { + b.setLength(0); + b.append("Bearer ").append(token); + get.addHeader(new BasicHeader("Authorization","Bearer " + token)); + } + HttpResponse resp = con.getHttp().execute(get); String json = EntityUtils.toString(resp.getEntity()); @@ -521,9 +558,13 @@ public String callWSDelete(String token, HttpCon con,String uri) throws IOExcept b.append(this.getUrl()).append(uri); HttpDelete get = new HttpDelete(b.toString()); - b.setLength(0); - b.append("Bearer ").append(token); - get.addHeader(new BasicHeader("Authorization","Bearer " + token)); + + if (token != null) { + b.setLength(0); + b.append("Bearer ").append(token); + get.addHeader(new BasicHeader("Authorization","Bearer " + token)); + } + HttpResponse resp = con.getHttp().execute(get); String json = EntityUtils.toString(resp.getEntity()); @@ -535,9 +576,12 @@ public String callWSPut(String token, HttpCon con,String uri,String json) throws b.append(this.getUrl()).append(uri); HttpPut put = new HttpPut(b.toString()); - b.setLength(0); - b.append("Bearer ").append(token); - put.addHeader(new BasicHeader("Authorization","Bearer " + token)); + + if (token != null) { + b.setLength(0); + b.append("Bearer ").append(token); + put.addHeader(new BasicHeader("Authorization","Bearer " + token)); + } StringEntity str = new StringEntity(json,ContentType.APPLICATION_JSON); put.setEntity(str); @@ -553,9 +597,12 @@ public String callWSPatchJson(String token, HttpCon con,String uri,String json) b.append(this.getUrl()).append(uri); HttpPatch put = new HttpPatch(b.toString()); - b.setLength(0); - b.append("Bearer ").append(token); - put.addHeader(new BasicHeader("Authorization","Bearer " + token)); + + if (token != null) { + b.setLength(0); + b.append("Bearer ").append(token); + put.addHeader(new BasicHeader("Authorization","Bearer " + token)); + } StringEntity str = new StringEntity(json,ContentType.create("application/merge-patch+json")); put.setEntity(str); @@ -571,9 +618,12 @@ public String callWSPost(String token, HttpCon con,String uri,String json) throw b.append(this.getUrl()).append(uri); HttpPost put = new HttpPost(b.toString()); - b.setLength(0); - b.append("Bearer ").append(token); - put.addHeader(new BasicHeader("Authorization","Bearer " + token)); + + if (token != null) { + b.setLength(0); + b.append("Bearer ").append(token); + put.addHeader(new BasicHeader("Authorization","Bearer " + token)); + } StringEntity str = new StringEntity(json,ContentType.APPLICATION_JSON); put.setEntity(str); @@ -601,16 +651,41 @@ public void init(Map cfg, ConfigManager cfgMgr, String name) } else { + String localTokenType = this.loadOptionalAttributeValue("tokenType", "tokenType",cfg,null); + + if (localTokenType == null) { + localTokenType = "LEGACY"; + } + + this.tokenType = TokenType.valueOf(localTokenType.toUpperCase()); - this.osToken = this.loadOptionalAttributeValue("token", "Token",cfg,"***************************"); + switch (tokenType) { + case STATIC: this.osToken = this.loadOptionalAttributeValue("token", "Token",cfg,"***************************"); break; + case LEGACY: + try { + this.osToken = new String(Files.readAllBytes(Paths.get("/var/run/secrets/kubernetes.io/serviceaccount/token")), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new ProvisioningException("Could not load token",e); + } + break; + case TOKENAPI: + this.tokenPath = this.loadOption("tokenPath", cfg, false); + this.checkProjectedToken(); + + break; + case NONE: + break; + case OIDC: + this.initRemoteOidc(cfg, cfgMgr, localTokenType); + break; + + } - if (this.osToken == null || this.osToken.isEmpty()) { + + + if (this.url.isEmpty()) { this.localToken = true; - try { - this.osToken = new String(Files.readAllBytes(Paths.get("/var/run/secrets/kubernetes.io/serviceaccount/token")), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new ProvisioningException("Could not load token",e); - } + String certAlias = this.loadOptionalAttributeValue("caCertAlias","caCertAlias", cfg,null); if (certAlias == null) { @@ -619,8 +694,13 @@ public void init(Map cfg, ConfigManager cfgMgr, String name) try { logger.info("Cert Alias Storing - '" + certAlias + "'"); - - X509Certificate cert = CertUtil.readCertificate("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"); + X509Certificate cert = null; + if (tokenType == TokenType.LEGACY) { + cert = CertUtil.readCertificate("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"); + } else if (tokenType == TokenType.TOKENAPI) { + // -\("/)/- + cert = CertUtil.readCertificate(this.loadOption("certPath", cfg, false)); + } logger.info("Certificate - " + cert); @@ -658,6 +738,54 @@ public void init(Map cfg, ConfigManager cfgMgr, String name) } + private void initRemoteOidc(Map cfg, ConfigManager cfgMgr, String name) throws ProvisioningException { + this.oidcIdp = this.loadOption("oidcIdp", cfg, false); + this.oidcIssuerHost = this.loadOptionalAttributeValue("oidcIssuerHost", "oidcIssuerHost", cfg, null); + this.oidcSub = this.loadOption("oidcSub", cfg, false); + this.oidcAudience = this.loadOption("oidcAudience", cfg, false); + + for (ApplicationType at : cfgMgr.getCfg().getApplications().getApplication()) { + if (at.getName().equals(this.oidcIdp)) { + for (ParamType pt : at.getUrls().getUrl().get(0).getIdp().getParams()) { + if (pt.getName().equals("jwtSigningKey")) { + this.oidcCertName = pt.getValue(); + } + } + + if (this.oidcIssuerHost == null) { + this.oidcIssuerHost = at.getUrls().getUrl().get(0).getHost().get(0); + } + + this.oidcIssuer = "https://" + this.oidcIssuerHost + at.getUrls().getUrl().get(0).getUri(); + + } + } + + + + } + + private synchronized void checkProjectedToken() throws ProvisioningException { + try { + if (this.tokenExpires == null || this.tokenExpires.isBeforeNow()) { + + this.osToken = new String(Files.readAllBytes(Paths.get(this.tokenPath)), StandardCharsets.UTF_8); + + int firstPeriod = this.osToken.indexOf('.'); + int lastPeriod = this.osToken.lastIndexOf('.'); + + String json = new String(Base64.decodeBase64(this.osToken.substring(firstPeriod + 1,lastPeriod))); + JSONObject claims = (JSONObject) new JSONParser().parse(json); + long exp = ((Long)claims.get("exp")) * 1000L; + this.tokenExpires = new DateTime(exp); + + + } + } catch (Exception e) { + throw new ProvisioningException("Could not generate new token",e); + } + } + private String loadOptionalAttributeValue(String name,String label,Map config,String mask) throws ProvisioningException { Attribute attr = config.get(name); if (attr == null) { @@ -749,7 +877,19 @@ public String getAuthToken() throws Exception { return token; } else { - return this.osToken; + + switch (this.tokenType) { + case NONE: return null; + case TOKENAPI: this.checkProjectedToken(); + case LEGACY: + case STATIC: return this.osToken; + case OIDC: return this.generateOidcToken(); + default: + throw new ProvisioningException("Unknown tokenType"); + + } + + } } finally { if (con != null) { @@ -758,6 +898,31 @@ public String getAuthToken() throws Exception { } } + private String generateOidcToken() throws JoseException { + JwtClaims claims = new JwtClaims(); + claims.setIssuer(this.oidcIssuer); + claims.setAudience(this.oidcAudience); + claims.setExpirationTimeMinutesInTheFuture(1); + claims.setNotBeforeMinutesInThePast(1); + claims.setGeneratedJwtId(); + claims.setIssuedAtToNow(); + claims.setSubject(this.oidcSub); + + JsonWebSignature jws = new JsonWebSignature(); + jws.setPayload(claims.toJson()); + jws.setKey(this.cfgMgr.getPrivateKey(this.oidcCertName)); + jws.setKeyIdHeaderValue(this.buildKID(this.cfgMgr.getCertificate(this.oidcCertName))); + jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); + + return jws.getCompactSerialization(); + } + + private String buildKID(X509Certificate cert) { + StringBuffer b = new StringBuffer(); + b.append(cert.getSubjectDN().getName()).append('-').append(cert.getIssuerDN().getName()).append('-').append(cert.getSerialNumber().toString()); + return b.toString(); + } + public void addUserToGroup(String token,HttpCon con,String userName,String groupName,int approvalID,Workflow workflow) throws Exception { Gson gson = new Gson(); StringBuffer b = new StringBuffer();