Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Real local dev #14

Merged
merged 8 commits into from
Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions ci/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ jobs:
run:
path: src/ci/test.sh

- name: build-test-images
- put: dev-elasticsearch-image
params:
build: src/docker/images/elasticsearch
dockerfile: src/docker/images/elasticsearch/dockerfile
tag_as_latest: true
cache: true

- put: dev-kibana-image
params:
build: src/docker/images/kibana
dockerfile: src/docker/images/kibana/dockerfile
tag_as_latest: true
cache: true

############################
# RESOURCES

Expand All @@ -56,6 +71,23 @@ resources:
uri: https://github.com/cloud-gov/((name))
branch: ((git-branch))

- name: dev-elasticsearch-image
type: docker-image
icon: docker
source:
email: ((docker-email))
username: ((docker-username))
password: ((docker-password))
repository: ((docker-image-elasticsearch-dev))

- name: dev-kibana-image
type: docker-image
icon: docker
source:
email: ((docker-email))
username: ((docker-username))
password: ((docker-password))
repository: ((docker-image-kibana-dev))

############################
# RESOURCE TYPES
Expand Down
12 changes: 12 additions & 0 deletions dev
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ main() {

${python} -m flask run -p ${PORT}
;;
cluster)

pushd docker
docker-compose up --force-recreate --build -d
popd

;;
destroy-cluster)
pushd docker
docker-compose down
popd
;;
watch-test|watch-tests)
watch_tests
;;
Expand Down
73 changes: 73 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
version: '3'
services:
odfe-node1:
build: ./elasticsearch
container_name: odfe-node1
environment:
- cluster.name=odfe-cluster
- node.name=odfe-node1
- discovery.seed_hosts=odfe-node1,odfe-node2
- cluster.initial_master_nodes=odfe-node1,odfe-node2
- bootstrap.memory_lock=true # along with the memlock settings below, disables swapping
- "ES_JAVA_OPTS=-Xms2048m -Xmx2048m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
- opendistro_security.audit.type=debug
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536 # maximum number of open files for the Elasticsearch user, set to at least 65536 on modern systems
hard: 65536
volumes:
- odfe-data1:/usr/share/elasticsearch/data
ports:
- 9200:9200
networks:
- odfe-net
odfe-node2:
build: ./elasticsearch
container_name: odfe-node2
environment:
- cluster.name=odfe-cluster
- node.name=odfe-node2
- discovery.seed_hosts=odfe-node1,odfe-node2
- cluster.initial_master_nodes=odfe-node1,odfe-node2
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms2048m -Xmx2048m"
- opendistro_security.audit.type=debug
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
- odfe-data2:/usr/share/elasticsearch/data
networks:
- odfe-net
kibana:
build: ./kibana
container_name: odfe-kibana
ports:
- 5601:5601
expose:
- "5601"
environment:
ELASTICSEARCH_URL: https://odfe-node1:9200
ELASTICSEARCH_HOSTS: https://odfe-node1:9200
elasticsearch.requestHeadersWhitelist: "securitytenant,Authorization,x-forwarded-for,x-proxy-user,x-proxy-roles"
openditsro.security.auth_type: "proxy"
opendistro.security.proxycache.user_header: "x-proxy-user"
opendistro.security.proxycache.roles_header: "x-proxy-roles"

apburnes marked this conversation as resolved.
Show resolved Hide resolved
networks:
- odfe-net
volumes: []

volumes:
odfe-data1:
odfe-data2:

networks:
odfe-net:
35 changes: 35 additions & 0 deletions docker/elasticsearch/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
_meta:
type: "config"
config_version: 2

config:
dynamic:
http:
xff:
enabled: true
internalProxies: ".*"
remoteIpHeader: "x-forwarded-for"
authc:
basic_internal_auth_domain:
description: "Authenticate via HTTP Basic against internal users database"
http_enabled: true
transport_enabled: true
order: 4
http_authenticator:
type: basic
challenge: true
authentication_backend:
type: intern
proxy_auth_domain:
http_enabled: true
transport_enabled: true
order: 0
http_authenticator:
type: proxy
challenge: false
config:
user_header: "x-proxy-user"
roles_header: "x-proxy-roles"
authentication_backend:
type: noop
30 changes: 30 additions & 0 deletions docker/elasticsearch/dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FROM amazon/opendistro-for-elasticsearch:1.12.0 as ODFE

# ok, this is a little weird.
# we're building this image to run on Cloud Foundry, where we can
# only have one port exposed. The upstream exposes more than one,
# and docker doesn't provide a direct method to override it.
# so, we're using the ODfE image as a "builder" and then copying
# everything into scratch, which is a blank image ¯\_(ツ)_/¯
FROM scratch

COPY --from=ODFE / /

# copying the files doesn't copy the metadata - that's the whole point
# these are the same as the upstream
WORKDIR /usr/share/elasticsearch
ENV JAVA_HOME /opt/jdk
ENV PATH $PATH:$JAVA_HOME/bin
ENV ELASTIC_CONTAINER true
USER 1000

# add the new stuff
COPY config.yml /usr/share/elasticsearch/plugins/opendistro_security/securityconfig/config.yml
COPY roles.yml /usr/share/elasticsearch/plugins/opendistro_security/securityconfig/roles.yml
COPY roles_mapping.yml /usr/share/elasticsearch/plugins/opendistro_security/securityconfig/roles_mapping.yml

EXPOSE 9200

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
# Dummy overridable parameter parsed by entrypoint
CMD ["eswrapper"]
20 changes: 20 additions & 0 deletions docker/elasticsearch/roles.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
cf_user:
reserved: false
hidden: false
cluster_permissions:
- "read"
- "cluster:monitor/nodes/stats"
- "cluster:monitor/task/get"
index_permissions:
- index_patterns:
- "logs-app-*"
dls: "{\"bool\": {\"should\": [{\"terms\": { \"@cf.space_id\": [\"$attr.proxy.space_ids\"] }}, {\"terms\": {\"@cf.org_id\": [\"$attr.proxy.org_ids\"]}}]}}"
fls:
allowed_actions:
- "read"
tenant_permissions: []
static: false
_meta:
type: "roles"
config_version: 2
16 changes: 16 additions & 0 deletions docker/elasticsearch/roles_mapping.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
_meta:
type: "rolesmapping"
config_version: 2
cf_user:
reserved: true
hidden: false
backend_roles:
- "user"
hosts: []
users: []
and_backend_roles: []
description: "Maps admin to all_access"
kibana_server:
reserved: true
users:
- "kibanaserver"
17 changes: 17 additions & 0 deletions docker/kibana/dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM amazon/opendistro-for-elasticsearch-kibana:1.12.0 AS kibana


FROM scratch

COPY --from=kibana / /

WORKDIR /usr/share/kibana

ENV PATH=/usr/share/kibana/bin:$PATH
USER 1000

EXPOSE 5601

COPY kibana.yml /usr/share/kibana/config/kibana.yml

CMD ["/usr/local/bin/kibana-docker"]
23 changes: 23 additions & 0 deletions docker/kibana/kibana.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
server.name: kibana
server.host: "0"
elasticsearch.hosts: https://localhost:9200
elasticsearch.ssl.verificationMode: none
elasticsearch.username: kibanaserver
elasticsearch.password: kibanaserver

opendistro_security.multitenancy.enabled: true
opendistro_security.multitenancy.tenants.preferred: ["Private", "Global"]
opendistro_security.readonly_mode.roles: ["kibana_read_only"]

# Use this setting if you are running kibana without https
opendistro_security.cookie.secure: false

newsfeed.enabled: false
telemetry.optIn: false
telemetry.enabled: false
security.showInsecureClusterWarning: false
elasticsearch.requestHeadersWhitelist: ["securitytenant","Authorization","x-forwarded-for","x-proxy-user","x-proxy-roles"]

opendistro_security.auth.type: "proxy"
opendistro_security.proxycache.user_header: "x-proxy-user"
opendistro_security.proxycache.roles_header: "x-proxy-roles"
18 changes: 15 additions & 3 deletions kibana_cf_auth_proxy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def callback():
return "logged in"

@app.route("/", defaults={"path": ""})
@app.route("/<path:path>")
@app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"])
def handle_request(path):
def redirect_to_auth():
session["state"] = urlsafe_b64encode(os.urandom(24)).decode("utf-8")
Expand All @@ -123,16 +123,28 @@ def redirect_to_auth():
url = f"{config.UAA_AUTH_URL}?{params}"
return redirect(url)

if session.get("user_id") is None:
allowed_paths = ["ui/favicons/manifest.json"]

if session.get("user_id") is None and path not in allowed_paths:
return redirect_to_auth()
forbidden_headers = {"host", "x-proxy-user", "x-proxy-ext-spaces"}

forbidden_headers = {"host", "x-proxy-user", "x-proxy-ext-space-ids"}
url = request.url.replace(request.host_url, config.KIBANA_URL)
headers = {
k: v
for k, v in request.headers.items()
if k.lower() not in forbidden_headers
}

# we need to check the user_id again because we could be unauthenticated, hitting an
# allowed path
if session.get("user_id"):
headers["x-proxy-user"] = session["user_id"]
headers["x-proxy-roles"] = "user"

# TODO: add x-forwarded-for functionality
headers["x-forwarded-for"] = "127.0.0.1"

return proxy_request(
url, headers, request.get_data(), request.cookies, request.method
)
Expand Down
2 changes: 2 additions & 0 deletions kibana_cf_auth_proxy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def __init__(self):
self.PERMITTED_ORG_ROLES = self.env_parser.list(
"PERMITTED_ORG_ROLES", ["org_manager"]
)
self.SESSION_COOKIE_NAME = "cfsession"


class UnitConfig(Config):
Expand Down Expand Up @@ -49,6 +50,7 @@ def __init__(self):

self.SESSION_REFRESH_EACH_REQUEST = True

self.CF_URL = self.env_parser("CF_URL")
self.UAA_AUTH_URL = self.env_parser.str("UAA_AUTH_URL")
self.UAA_TOKEN_URL = self.env_parser.str("UAA_TOKEN_URL")
self.UAA_CLIENT_ID = self.env_parser.str("UAA_CLIENT_ID")
Expand Down
7 changes: 6 additions & 1 deletion kibana_cf_auth_proxy/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ def proxy_request(url, headers, data, cookies, method):
for (name, value) in resp.raw.headers.items()
if name.lower() not in excluded_headers
]
kwargs = {}
if resp.raw.headers.get("content-encoding") == "br":
headers.append(("content-encoding", "br"),)
if "content-type" in resp.headers:
kwargs["content_type"] = resp.headers["content-type"]

response = Response(resp.content, resp.status_code, headers)
response = Response(resp.content, resp.status_code, headers, **kwargs)
return response
16 changes: 13 additions & 3 deletions tests/unit/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,23 @@ def test_app_proxies_arbitrary_paths(authenticated_client):


def test_app_filters_headers(authenticated_client):
"""
if we send one of the permission-related headers with our own value
it should be dropped or changed
"""
with requests_mock.Mocker() as m:
m.get("mock://kibana/foo/bar/baz/quux/")
authenticated_client.get(
"/foo/bar/baz/quux/", headers={"X-pRoXy-UsEr": "administrator"}
"/foo/bar/baz/quux/", headers={"X-pRoXy-UsEr": "administrator", "x-proxy-roles": "batman", "x-proxy-ext-space-ids": "1,2,3"}
)
for header in m.request_history[0]._request.headers:
assert header.lower() != "x-proxy-user"
if header.lower() == "x-proxy-user":
assert m.request_history[0]._request.headers[header] != "administrator"
if header.lower() == "x-proxy-roles":
assert m.request_history[0]._request.headers[header] != "batman"
if header.lower() == "x-proxy-ext-space-ids":
assert m.request_history[0]._request.headers[header] != "1,2,3"



def test_session_refreshes(client):
Expand All @@ -60,7 +70,7 @@ def test_session_refreshes(client):
client.get("/ping")
session_interface = flask.current_app.session_interface
cache = session_interface.cache
session_id = client.cookie_jar._cookies["localhost.local"]["/"]["session"].value
session_id = client.cookie_jar._cookies["localhost.local"]["/"]["cfsession"].value
session_backing = session_interface.key_prefix + session_id
filename = cache._get_filename(session_backing)
with open(filename, "rb") as f:
Expand Down