diff --git a/templates/.circleci/config.yml b/templates/.circleci/config.yml index c4e4da9..e7e1736 100644 --- a/templates/.circleci/config.yml +++ b/templates/.circleci/config.yml @@ -252,7 +252,7 @@ jobs: cd kubernetes/overlays/<< parameters.config-environment >> IMAGE=<< parameters.account-id >>.dkr.ecr.<< parameters.region >>.amazonaws.com/<< parameters.repo >> kustomize edit set image fake-image=${IMAGE}:${VERSION_TAG} - kustomize build . | kubectl apply -f - -n $NAMESPACE + kustomize build . | kubectl apply -f - if ! kubectl -n $NAMESPACE rollout status deployment/$DEPLOYMENT -w --timeout=180s ; then echo "$DEPLOYMENT rollout check failed:" echo "$DEPLOYMENT deployment:" diff --git a/templates/kubernetes/base/auth.yml b/templates/kubernetes/base/auth.yml new file mode 100644 index 0000000..1b02a3d --- /dev/null +++ b/templates/kubernetes/base/auth.yml @@ -0,0 +1,87 @@ +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: kratos-public +spec: + upstream: + url: http://kratos-public.user-auth + stripPath: /.ory/kratos/public + preserveHost: true + match: + #url: http:///.ory/kratos/public/<.*> + methods: + - GET + - POST + - PUT + - DELETE + - PATCH + authenticators: + - handler: noop + authorizer: + handler: allow + mutators: + - handler: noop +--- +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: kratos-form-data +spec: + upstream: + url: http://kratos-admin.user-auth + stripPath: /.ory/kratos + preserveHost: true + match: + #url: http:///.ory/kratos/self-service/<(login|registration|recovery|settings)>/flows<.*> + methods: + - GET + authenticators: + - handler: noop + authorizer: + handler: allow + mutators: + - handler: noop +--- +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: public-backend-endpoints +spec: + version: test + upstream: + url: http://<% .Name %>.<% .Name %> + preserveHost: true + match: + # url: http:///status/<.*> + methods: + - GET + - POST + authenticators: + - handler: noop + authorizer: + handler: allow + mutators: + - handler: noop +--- +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: authenticated-backend-endpoints +spec: + version: test + upstream: + preserveHost: true + url: http://<% .Name %>.<% .Name %> + stripPath: /api + match: + # url: /api/<.*> + methods: + - GET + - POST + authenticators: + - handler: cookie_session + authorizer: + handler: allow + mutators: + - handler: id_token + - handler: header diff --git a/templates/kubernetes/base/kustomization.yml b/templates/kubernetes/base/kustomization.yml index a46d9ec..8acec64 100644 --- a/templates/kubernetes/base/kustomization.yml +++ b/templates/kubernetes/base/kustomization.yml @@ -1,6 +1,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization +namespace: <% .Name %> + resources: - deployment.yml - service.yml diff --git a/templates/kubernetes/overlays/production/auth.yml b/templates/kubernetes/overlays/production/auth.yml new file mode 100644 index 0000000..32ee4e7 --- /dev/null +++ b/templates/kubernetes/overlays/production/auth.yml @@ -0,0 +1,31 @@ +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: kratos-public +spec: + match: + url: http://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>/.ory/kratos/public/<.*> +--- +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: kratos-form-data +spec: + match: + url: http://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>/.ory/kratos/self-service/<(login|registration|recovery|settings)>/flows<.*> +--- +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: public-backend-endpoints +spec: + match: + url: http://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>/<(?!(api|\.ory\/kratos)).*> +--- +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: authenticated-backend-endpoints +spec: + match: + url: http://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>/api/<.*> diff --git a/templates/kubernetes/overlays/production/ingress.yml b/templates/kubernetes/overlays/production/ingress.yml index 6f27420..c951d70 100644 --- a/templates/kubernetes/overlays/production/ingress.yml +++ b/templates/kubernetes/overlays/production/ingress.yml @@ -2,6 +2,7 @@ apiVersion: extensions/v1beta1 kind: Ingress metadata: name: <% .Name %> + namespace: <% .Name %> annotations: # nginx ingress kubernetes.io/ingress.class: nginx diff --git a/templates/kubernetes/overlays/production/kustomization.yml b/templates/kubernetes/overlays/production/kustomization.yml index 34662fe..9b80d3f 100644 --- a/templates/kubernetes/overlays/production/kustomization.yml +++ b/templates/kubernetes/overlays/production/kustomization.yml @@ -3,11 +3,13 @@ kind: Kustomization patchesStrategicMerge: - deployment.yml - +<%if eq (index .Params `userAuth`) "yes" %>- auth.yml +<% end %> resources: - ../../base -- ingress.yml -- pdb.yml +<%if eq (index .Params `userAuth`) "yes" %>#<% end %>- ingress.yml +<%if eq (index .Params `userAuth`) "yes" %>- auth.yml +<% end %> configMapGenerator: - name: <% .Name %>-config diff --git a/templates/kubernetes/overlays/production/pdb.yml b/templates/kubernetes/overlays/production/pdb.yml index a75a1a4..e640f4e 100644 --- a/templates/kubernetes/overlays/production/pdb.yml +++ b/templates/kubernetes/overlays/production/pdb.yml @@ -3,6 +3,7 @@ apiVersion: policy/v1beta1 kind: PodDisruptionBudget metadata: name: <% .Name %> + namespace: <% .Name %> spec: minAvailable: 2 selector: diff --git a/templates/kubernetes/overlays/staging/auth.yml b/templates/kubernetes/overlays/staging/auth.yml new file mode 100644 index 0000000..7dd0aff --- /dev/null +++ b/templates/kubernetes/overlays/staging/auth.yml @@ -0,0 +1,31 @@ +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: kratos-public +spec: + match: + url: http://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>/.ory/kratos/public/<.*> +--- +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: kratos-form-data +spec: + match: + url: http://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>/.ory/kratos/self-service/<(login|registration|recovery|settings)>/flows<.*> +--- +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: public-backend-endpoints +spec: + match: + url: http://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>/<(?!(api|\.ory\/kratos)).*> +--- +apiVersion: oathkeeper.ory.sh/v1alpha1 +kind: Rule +metadata: + name: authenticated-backend-endpoints +spec: + match: + url: http://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>/api/<.*> diff --git a/templates/kubernetes/overlays/staging/ingress.yml b/templates/kubernetes/overlays/staging/ingress.yml index 156d827..336f091 100644 --- a/templates/kubernetes/overlays/staging/ingress.yml +++ b/templates/kubernetes/overlays/staging/ingress.yml @@ -2,6 +2,7 @@ apiVersion: extensions/v1beta1 kind: Ingress metadata: name: <% .Name %> + namespace: <% .Name %> annotations: # nginx ingress kubernetes.io/ingress.class: nginx diff --git a/templates/kubernetes/overlays/staging/kustomization.yml b/templates/kubernetes/overlays/staging/kustomization.yml index a375f26..d31ed90 100644 --- a/templates/kubernetes/overlays/staging/kustomization.yml +++ b/templates/kubernetes/overlays/staging/kustomization.yml @@ -3,10 +3,13 @@ kind: Kustomization patchesStrategicMerge: - deployment.yml - +<%if eq (index .Params `userAuth`) "yes" %>- auth.yml +<% end %> resources: - ../../base -- ingress.yml +<%if eq (index .Params `userAuth`) "yes" %>#<% end %>- ingress.yml +<%if eq (index .Params `userAuth`) "yes" %>- auth.yml +<% end %> configMapGenerator: - name: <% .Name %>-config diff --git a/templates/src/app.js b/templates/src/app.js index 4b0a34f..6fc09f2 100644 --- a/templates/src/app.js +++ b/templates/src/app.js @@ -1,63 +1,20 @@ -var aws = require("aws-sdk"); -var cfsign = require("aws-cloudfront-sign"); +var dotenv = require("dotenv"); var express = require("express"); var morgan = require("morgan"); var { connect } = require("./db"); - +const statusRoutes = require("./app/status"); +<%if eq (index .Params `fileUploads`) "yes" %>const fileRoutes = require("./app/file"); +<% end %><%if eq (index .Params `userAuth`) "yes" %>const authRoutes = require("./app/auth"); +<% end %> +dotenv.config(); var app = express(); app.use(morgan("combined")); -var s3 = new aws.S3(); - -app.get("/presigned/:key", (req, res) => { - var params = { - Bucket: process.env.S3_BUCKET, - Fields: { - key: req.params.key, - }, - }; - - s3.createPresignedPost(params, (err, data) => { - if (err) { - console.error(err); - res.sendStatus(500); - } else { - console.log(data); - res.send(data); - } - }); -}); - -app.get("/:key", (req, res) => { - var params = { - keypairId: process.env.CF_KEYPAIR_ID, - privateKeyString: process.env.CF_KEYPAIR_SECRET_KEY, - expireTime: new Date().getTime() + 30000, // defaults to 30s - }; - - var url = cfsign.getSignedUrl( - `https://files.${process.env.DOMAIN}/${req.params.key}`, - params - ); - - console.log(url); - res.redirect(url); -}); - -app.get("/status/ready", (req, res) => { - res.send("OK"); -}); - -app.get("/status/alive", (req, res) => { - res.send("OK"); -}); - -app.get("/status/about", (req, res) => { - res.send({ - podName: process.env.POD_NAME, - }); -}); +app.use("/status", statusRoutes); +<%if eq (index .Params `userAuth`) "yes" %>app.use("/auth", authRoutes); +<% end %><%if eq (index .Params `fileUploads`) "yes" %>app.use("/file", fileRoutes); +<% end %> var port = process.env.SERVER_PORT; if (!port) { port = 3000; diff --git a/templates/src/app/auth/index.js b/templates/src/app/auth/index.js new file mode 100644 index 0000000..067a802 --- /dev/null +++ b/templates/src/app/auth/index.js @@ -0,0 +1,11 @@ +var { Router } = require("express"); + +var { authMiddleware } = require("../../middleware/auth"); + +var router = Router() + +router.get("/userInfo", authMiddleware, (req, res) => { + res.json(req.user); +}); + +module.exports = router; diff --git a/templates/src/app/file/index.js b/templates/src/app/file/index.js new file mode 100644 index 0000000..8fd02b5 --- /dev/null +++ b/templates/src/app/file/index.js @@ -0,0 +1,43 @@ +var { Router } = require("express"); +var aws = require("aws-sdk"); +var cfsign = require("aws-cloudfront-sign"); + +var router = Router() +var s3 = new aws.S3(); + +router.get("/presigned/:key", (req, res) => { + var params = { + Bucket: process.env.S3_BUCKET, + Fields: { + key: req.params.key, + }, + }; + + s3.createPresignedPost(params, (err, data) => { + if (err) { + console.error(err); + res.sendStatus(500); + } else { + console.log(data); + res.send(data); + } + }); +}); + +router.get("/:key", (req, res) => { + var params = { + keypairId: process.env.CF_KEYPAIR_ID, + privateKeyString: process.env.CF_KEYPAIR_SECRET_KEY, + expireTime: new Date().getTime() + 30000, // defaults to 30s + }; + + var url = cfsign.getSignedUrl( + `https://files.${process.env.DOMAIN}/${req.params.key}`, + params + ); + + console.log(url); + res.redirect(url); +}); + +module.exports = router; diff --git a/templates/src/app/status/index.js b/templates/src/app/status/index.js new file mode 100644 index 0000000..bb1b41e --- /dev/null +++ b/templates/src/app/status/index.js @@ -0,0 +1,19 @@ +var { Router } = require("express"); + +var router = Router() + +router.get("/ready", (req, res) => { + res.send("OK"); +}); + +router.get("/alive", (req, res) => { + res.send("OK"); +}); + +router.get("/about", (req, res) => { + res.send({ + podName: process.env.POD_NAME, + }); +}); + +module.exports = router; diff --git a/templates/src/middleware/auth/index.js b/templates/src/middleware/auth/index.js new file mode 100644 index 0000000..3d16e6e --- /dev/null +++ b/templates/src/middleware/auth/index.js @@ -0,0 +1,25 @@ +const authMiddleware = (req, res, next) => { + /** Expecting oathkeeper to pass on user identity in headers + * (note nodejs converts all headers to lowercase) + * 1. X-User-Id + * 2. X-User-Email + * */ + const hasUserData = req.headers["x-user-id"] && req.headers["x-user-email"]; + if (!hasUserData) { + res.status(401); + res.json({ + success: false, + message: "unauthenticated", + }); + } else { + req.user = { + id: req.headers["x-user-id"], + email: req.headers["x-user-email"], + }; + next(); + } +}; + +module.exports = { + authMiddleware, +}; diff --git a/zero-module.yml b/zero-module.yml index bf54f05..4c5174b 100644 --- a/zero-module.yml +++ b/zero-module.yml @@ -84,3 +84,25 @@ parameters: options: - "yes" - "no" + - field: userAuth + label: Enable user management using Kratos and authentication using the Oathkeeper access proxy? + default: yes + options: + - "yes" + - "no" + +conditions: + - action: ignoreFile + matchField: fileUploads + whenValue: "no" + data: + - src/app/file + - action: ignoreFile + matchField: userAuth + whenValue: "no" + data: + - src/middleware/auth + - src/app/auth + - kubernetes/base/auth.yml + - kubernetes/overlays/staging/auth.yml + - kubernetes/overlays/production/auth.yml