From e6a2c646d8a35f7e65b5fb0fb2373a36a458b12a Mon Sep 17 00:00:00 2001 From: Maurice Faber Date: Fri, 20 Nov 2020 14:21:19 +0100 Subject: [PATCH] refactor(oidc): moved oidc.idp props to oidc, added home [ci skip] --- .demo/env/charts/keycloak.yaml | 1 + .demo/env/charts/secrets.keycloak.yaml | 4 ++ .demo/env/secrets.settings.yaml | 6 +-- .demo/env/settings.yaml | 28 ++++++----- .values/.vscode/settings.json | 1 + .vscode/settings.json | 3 +- bin/otomi | 1 + helmfile.d/helmfile-10.monitoring.yaml | 2 +- helmfile.d/helmfile-60.teams.yaml | 8 ++-- helmfile.d/snippets/alertmanager.gotmpl | 12 ++--- helmfile.d/snippets/defaults.gotmpl | 1 + helmfile.d/snippets/env.gotmpl | 3 -- values-schema.yaml | 47 +++++++++---------- .../istio-operator/istio-operator-raw.gotmpl | 26 ++++++---- values/jobs/harbor.gotmpl | 2 +- values/jobs/keycloak.gotmpl | 16 +++---- values/k8s/k8s-raw.gotmpl | 26 +++++----- values/oauth2-proxy/oauth2-proxy.gotmpl | 2 +- values/otomi-api/otomi-api.gotmpl | 2 +- .../prometheus-operator.gotmpl | 7 ++- 20 files changed, 108 insertions(+), 90 deletions(-) create mode 100644 .demo/env/charts/secrets.keycloak.yaml diff --git a/.demo/env/charts/keycloak.yaml b/.demo/env/charts/keycloak.yaml index 012a8c3202..5fa4bcdfdf 100644 --- a/.demo/env/charts/keycloak.yaml +++ b/.demo/env/charts/keycloak.yaml @@ -4,4 +4,5 @@ charts: theme: otomi realm: master idp: + clientID: otomi alias: redkubes-azure diff --git a/.demo/env/charts/secrets.keycloak.yaml b/.demo/env/charts/secrets.keycloak.yaml new file mode 100644 index 0000000000..8685eee042 --- /dev/null +++ b/.demo/env/charts/secrets.keycloak.yaml @@ -0,0 +1,4 @@ +charts: + keycloak: + idp: + clientSecret: somsecretvalue diff --git a/.demo/env/secrets.settings.yaml b/.demo/env/secrets.settings.yaml index b59525dc6f..d0b6fff5b3 100644 --- a/.demo/env/secrets.settings.yaml +++ b/.demo/env/secrets.settings.yaml @@ -20,9 +20,9 @@ clouds: "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/dnsmanager%40otomi-cloud.iam.gserviceaccount.com" } +home: + slack: + url: https://hooks.slack.com/services/id alerts: - home: - slack: - url: https://hooks.slack.com/services/id slack: url: https://hooks.slack.com/services/id diff --git a/.demo/env/settings.yaml b/.demo/env/settings.yaml index b9fd4c9453..b6eec6c8af 100644 --- a/.demo/env/settings.yaml +++ b/.demo/env/settings.yaml @@ -8,19 +8,21 @@ otomi: customer: name: demo oidc: - clientID: otomi - idp: - issuer: https://login.microsoftonline.com/57a3f6ea-7e70-4260-acb4-e06ce452f695 - tenantID: 57a3f6ea-7e70-4260-acb4-e06ce452f695 - clientID: someClientID - adminGroupID: someAdminGroupID - teamAdminGroupID: someTeamAdminGroupID + clientID: someClientID + clientSecret: someClientSecret + issuer: https://login.microsoftonline.com/57a3f6ea-7e70-4260-acb4-e06ce452f695 + tenantID: 57a3f6ea-7e70-4260-acb4-e06ce452f695 + adminGroupID: someAdminGroupID + teamAdminGroupID: someTeamAdminGroupID scope: openid email profile +home: + receivers: [slack] + slack: + channel: mon-otomi + channelCrit: mon-otomi-crit alerts: drone: slack - home: - receivers: [slack] - slack: - channel: mon-otomi - channelCrit: mon-otomi-crit - receivers: [slack] + receivers: [slack, email] + email: + from: admins@your.cloud + smarthost: some.smtp.host diff --git a/.values/.vscode/settings.json b/.values/.vscode/settings.json index 92b628ae72..202ef6e5e9 100644 --- a/.values/.vscode/settings.json +++ b/.values/.vscode/settings.json @@ -17,6 +17,7 @@ "prettier.enable": true, "sops.defaults.gcpCredentialsPath": "gcp-key.json", "yaml.schemas": { + "http://json-schema.org/draft/2019-09/schema#": ".vscode/values-schema.yaml", ".vscode/values-schema.yaml": "env/*.yaml" } } diff --git a/.vscode/settings.json b/.vscode/settings.json index 78abbb949d..19bfe4e502 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,7 +24,8 @@ "CONTRIBUTING": "markdown" }, "yaml.schemas": { - "http://json-schema.org/draft/2019-09/schema#": "values-schema.yaml", + "http://json-schema.org/draft/2019-09/schema#": "./values-schema.yaml", + "http://json-schema.org/draft/2019-09/schema#": ".vscode/values-schema.yaml", ".values/values-schema.yaml": ".demo/env/*.yaml" }, "shellformat.flag": "-i 2 -ci" diff --git a/bin/otomi b/bin/otomi index 45e0e2b42b..08d7d0e9af 100755 --- a/bin/otomi +++ b/bin/otomi @@ -122,6 +122,7 @@ function validate_k8s_context() { local context=$(kubectl config current-context) if [[ "$K8S_CONTEXT" != "$context" ]]; then echo "Warning: Your current kubernetes context does not match target context: $K8S_CONTEXT" + echo "" read -p "Would you like to switch kube context to target first? Yn" oki if [ "${oki:-y}" = "y" ]; then kubectl config use $K8S_CONTEXT diff --git a/helmfile.d/helmfile-10.monitoring.yaml b/helmfile.d/helmfile-10.monitoring.yaml index bab2ab501f..1567c75057 100644 --- a/helmfile.d/helmfile-10.monitoring.yaml +++ b/helmfile.d/helmfile-10.monitoring.yaml @@ -23,7 +23,7 @@ releases: namespace: monitoring <<: *default - name: prometheus-msteams - installed: {{ or (eq ($v.alerts | get "receiver" "slack") "msteams") (eq ($v.alerts | get "home.receiver" "slack") "msteams") }} + installed: {{ or (eq ($v.alerts | get "receiver" "slack") "msteams") (eq ($v.home | get "receiver" "slack") "msteams") }} namespace: monitoring <<: *default - name: sitespeed diff --git a/helmfile.d/helmfile-60.teams.yaml b/helmfile.d/helmfile-60.teams.yaml index 3b1e91ce10..63edb6c2c9 100644 --- a/helmfile.d/helmfile-60.teams.yaml +++ b/helmfile.d/helmfile-60.teams.yaml @@ -50,7 +50,7 @@ releases: {{- end }} {{- if $v.otomi.isMultitenant }} - name: prometheus-{{ $teamId }} - installed: true + installed: {{ has $v.cluster.id $team.clusters }} namespace: team-{{ $teamId }} chart: ../charts/prometheus-operator labels: @@ -172,7 +172,7 @@ releases: {{- end }} {{ if has "msteams" ($team | get "receivers" list) }} - name: prometheus-msteams-{{ $teamId }} - installed: true + installed: {{ has $v.cluster.id $team.clusters }} namespace: team-{{ $teamId }} chart: ../charts/prometheus-msteams labels: @@ -192,7 +192,7 @@ releases: {{- end }} - name: grafana-dashboards-{{ $teamId }} - installed: true + installed: {{ has $v.cluster.id $team.clusters }} namespace: team-{{ $teamId }} chart: ../charts/grafana-dashboards labels: @@ -205,7 +205,7 @@ releases: folders: - k8s - istio - {{- if and (eq $v.cluster.provider "azure") (hasKey $azure "monitor") }} + {{- if and (eq $v.cluster.provider "azure") ($team | get "azure.monitor" ($v.clouds | get "azure.monitor" nil)) }} - azure{{ end }} {{- if $team | get "stack.sitespeed" false }} - sitespeed{{ end }} diff --git a/helmfile.d/snippets/alertmanager.gotmpl b/helmfile.d/snippets/alertmanager.gotmpl index 4ed57d1446..4de3bdb85f 100644 --- a/helmfile.d/snippets/alertmanager.gotmpl +++ b/helmfile.d/snippets/alertmanager.gotmpl @@ -1,8 +1,8 @@ {{- $receivers := .instance | get "alerts.receivers" (.root | get "alerts.receivers" (list "slack")) }} {{- $suffix := (true | ternary "" ".monitoring.svc.cluster.local") }} global: -{{- if or (has "slack" $receivers ) (and .root.otomi.isHomeMonitored (.root.alerts | get "home.receivers" (list "slack"))) }} - slack_api_url: {{ .instance | get "alerts.slack.url" (.root | get "alerts.slack.url" (.root | get "alerts.home.slack.url")) }} +{{- if or (has "slack" $receivers ) (and .root.otomi.isHomeMonitored (.root | get "home.receivers" (list "slack"))) }} + slack_api_url: {{ .instance | get "alerts.slack.url" (.root | get "alerts.slack.url" (.root | get "home.slack.url")) }} {{- end }} {{- if has "email" $receivers }} smtp_smarthost: {{ .instance | get "alerts.email.smarthost" (.root | get "alerts.email.smarthost") }} @@ -76,11 +76,11 @@ receivers: {{- end }} {{- if .root.otomi.isHomeMonitored }} - name: critical-home - {{- $receivers := .root.alerts.home | get "receivers" }} + {{- $receivers := .root.home | get "receivers" }} # sending criticals also to home to be aware of issues {{- if has "slack" $receivers }} slack_configs: - - channel: "#{{ .root | get "alerts.home.slack.channelCrit" "mon-otomi-crit" }}" + - channel: "#{{ .root | get "home.slack.channelCrit" "mon-otomi-crit" }}" {{- .slackTpl | nindent 8 }} {{- end }} {{- if has "msteams" $receivers }} @@ -90,8 +90,8 @@ receivers: {{- end }} {{- if has "email" $receivers }} email_configs: - - to: {{ .root | get "alerts.home.email.to" }} - from: {{ .root | get "alerts.home.email.from" (print "alerts@" .root.cluster.domain) }} + - to: {{ .root | get "home.email.to" }} + from: {{ .root | get "home.email.from" (print "alerts@" .root.cluster.domain) }} send_resolved: true {{- end }} {{- end }} diff --git a/helmfile.d/snippets/defaults.gotmpl b/helmfile.d/snippets/defaults.gotmpl index a89a0a53a4..728d8e6c3d 100644 --- a/helmfile.d/snippets/defaults.gotmpl +++ b/helmfile.d/snippets/defaults.gotmpl @@ -12,6 +12,7 @@ environments: # toYaml | fromYaml avoids bug that does not let us do a merge in a simple way: https://github.com/roboll/helmfile/issues/1275 {{- $values := $v | toYaml | fromYaml -}} {{- $clusterSettings := $c | toYaml | fromYaml -}} + {{- $clusterSettings = set $clusterSettings "id" (printf "%s/%s" $provider $clusterName) -}} {{- $clusterSettings = set $clusterSettings "provider" $provider -}} {{- $clusterSettings = set $clusterSettings "name" $clusterName -}} {{- $clusterSettings = set $clusterSettings "domain" (printf "%s.%s" $clusterName $p.domain) -}} diff --git a/helmfile.d/snippets/env.gotmpl b/helmfile.d/snippets/env.gotmpl index 4950c0ce48..0a91e4c68f 100644 --- a/helmfile.d/snippets/env.gotmpl +++ b/helmfile.d/snippets/env.gotmpl @@ -1,10 +1,7 @@ {{- $config := readFile "../env/env/teams.yaml" | fromYaml }} # toYaml | fromYaml is workaround for a bug: https://github.com/roboll/helmfile/issues/1275 {{- $teams := (index $config "teamConfig" "teams") | toYaml | fromYaml | keys}} - - {{- $sops := (exec "bash" (list "-c" "( test -f ../env/.sops.yaml && echo 'enabled: true' ) || echo 'enabled: false'")) | fromYaml }} - {{- $v := . -}} {{- $charts := (exec "bash" (list "-c" "find ../env/env/charts -name '*.yaml' -not -name 'secrets.*.yaml'")) | splitList "\n" }} {{ printf "%s-%s" .cluster.provider .cluster.name }}: diff --git a/values-schema.yaml b/values-schema.yaml index ffca5ff154..07331b50e7 100644 --- a/values-schema.yaml +++ b/values-schema.yaml @@ -173,6 +173,7 @@ definitions: type: string pattern: ^(https:\/\/)([\w\-])+\.{1}([a-zA-Z]{2,63})([\/\w-]*)*\/?\??([^#\n\r]*)?#?([^\n\r]*)$ alerts: + type: object properties: drone: type: string @@ -190,10 +191,12 @@ definitions: description: How long to wait before sending a notification again if it has already been sent successfully for an alert. (Usually ~3h or more). receivers: type: array - enum: - - slack - - msteams - - email + items: + type: string + enum: + - slack + - msteams + - email description: Notification receivers. slack: type: object @@ -241,7 +244,7 @@ definitions: type: string auth_identity: type: string - + required: [smarthost, to] required: [receivers] cloud: description: A common cloud configuration @@ -537,12 +540,10 @@ definitions: required: [id, name] required: [id] properties: + home: + '$ref': '#/definitions/alerts' alerts: '$ref': '#/definitions/alerts' - additionalProperties: - home: - description: Configuration to phone home. Used when otomi.isHomeMonitored is set. - '$ref': '#/definitions/alerts' charts: type: object additionalProperties: false @@ -994,6 +995,10 @@ properties: properties: alias: type: string + clientID: + type: string + clientSecret: + type: string realm: type: string theme: @@ -1155,24 +1160,16 @@ properties: type: string clientSecret: type: string - idp: - type: object - additionalProperties: false - properties: - adminGroupID: - type: string - clientID: - type: string - clientSecret: - type: string - issuer: - type: string - teamAdminGroupID: - type: string - tenantID: - type: string + issuer: + type: string + tenantID: + type: string scope: type: string + adminGroupID: + type: string + teamAdminGroupID: + type: string otomi: type: object additionalProperties: false diff --git a/values/istio-operator/istio-operator-raw.gotmpl b/values/istio-operator/istio-operator-raw.gotmpl index fa40e9a9de..366242f841 100644 --- a/values/istio-operator/istio-operator-raw.gotmpl +++ b/values/istio-operator/istio-operator-raw.gotmpl @@ -240,26 +240,34 @@ resources: GF_SERVER_ROOT_URL: /grafana-istio/ contextPath: '/grafana-istio' grafana.ini: + {{ if not $hasOIDC }} "auth.anonymous": - enabled: {{ not $hasOIDC }} - # enabled: true + enabled: true org_role: Admin org_name: Main Org. + {{- else }} "auth.generic_oauth": - # enabled: false - enabled: {{ $hasOIDC }} + tls_skip_verify_insecure: {{ eq ($v.charts | get "cert-manager.stage") "staging" }} + enabled: true name: OAuth org_role: Admin allow_sign_up: true # allow_sign_up: false oauth_auto_login: true # false = so we can login with admin / bladibla - client_id: {{ $o.clientID }} - client_secret: {{ $o.clientSecret }} + client_id: {{ $v.oidc.clientID }} + client_secret: {{ $v.oidc.clientSecret }} scopes: openid - auth_url: {{ $hasKeycloak | ternary (printf "%s/protocol/openid-connect/auth" $keycloakBase) ($o | getOrNil "grafana.authUrl") }} - token_url: {{ $hasKeycloak | ternary (printf "%s/protocol/openid-connect/token" $keycloakBase) ($o | getOrNil "grafana.tokenUrl") }} - api_url: {{ $hasKeycloak | ternary (printf "%s/protocol/openid-connect/userinfo" $keycloakBase) ($o | getOrNil "grafana.apiUrl") }} + auth_url: {{ $hasKeycloak | ternary (printf "%s/protocol/openid-connect/auth" $keycloakBase) ($v.oidc | getOrNil "grafana.authUrl") }} + token_url: {{ $hasKeycloak | ternary (printf "%s/protocol/openid-connect/token" $keycloakBase) ($v.oidc | getOrNil "grafana.tokenUrl") }} + api_url: {{ $hasKeycloak | ternary (printf "%s/protocol/openid-connect/userinfo" $keycloakBase) ($v.oidc | getOrNil "grafana.apiUrl") }} role_attribute_path: contains(groups[*], 'admin') && 'Admin' || contains(groups[*], 'team-admin') && 'Admin' || 'Editor' + {{- end }} + log: + level: error + users: + allow_sign_up: false + auto_assign_org: true + auto_assign_org_role: Viewer meshConfig: enableAutoMtls: true accessLogFile: "/dev/stdout" diff --git a/values/jobs/harbor.gotmpl b/values/jobs/harbor.gotmpl index 2f25821f11..7fa57ffda6 100644 --- a/values/jobs/harbor.gotmpl +++ b/values/jobs/harbor.gotmpl @@ -39,7 +39,7 @@ tasks: env: HARBOR_BASE_URL: "http://harbor-harbor-core.harbor/api/v2.0" TEAM_NAMES: '{{ $teamNames | toJson }}' - OIDC_ENDPOINT: '{{ $hasKeycloak | ternary $keycloakIssuer $o.idp.issuer }}' + OIDC_ENDPOINT: '{{ $hasKeycloak | ternary $keycloakIssuer $o.issuer }}' OIDC_GROUPS_CLAIM: 'groups' OIDC_NAME: 'keycloak' OIDC_SCOPE: 'openid' diff --git a/values/jobs/keycloak.gotmpl b/values/jobs/keycloak.gotmpl index 33589b116c..0ada668ed9 100644 --- a/values/jobs/keycloak.gotmpl +++ b/values/jobs/keycloak.gotmpl @@ -11,7 +11,6 @@ {{- $c := $v.charts }} {{- $cm := $c | get "cert-manager" -}} {{- $k := $c | get "keycloak" dict }} -{{- $idp := $v.oidc.idp }} {{- $skipVerify := eq ($cm | get "stage") "staging" }} tasks: keycloak: @@ -40,15 +39,16 @@ tasks: KEYCLOAK_ADMIN: {{ $k | get "admin.username" "admin" }} KEYCLOAK_ADMIN_PASSWORD: {{ $k | get "admin.password" "bladibla" }} KEYCLOAK_REALM: master - KEYCLOAK_CLIENT_SECRET: {{ $v.oidc.clientSecret }} - TENANT_ID: {{ $idp.tenantID }} - TENANT_CLIENT_ID: {{ $idp.clientID }} - TENANT_CLIENT_SECRET: {{ $idp.clientSecret }} + KEYCLOAK_CLIENT_ID: {{ $k.idp | get "clientID" "otomi" }} + KEYCLOAK_CLIENT_SECRET: {{ $k.idp.clientSecret }} + TENANT_ID: {{ $v.oidc.tenantID }} + TENANT_CLIENT_ID: {{ $v.oidc.clientID }} + TENANT_CLIENT_SECRET: {{ $v.oidc.clientSecret }} IDP_ALIAS: {{ $k.idp.alias }} - IDP_GROUP_OTOMI_ADMIN: {{ $idp.adminGroupID }} - IDP_GROUP_TEAM_ADMIN: {{ $idp.teamAdminGroupID }} + IDP_GROUP_OTOMI_ADMIN: {{ $v.oidc.adminGroupID }} + IDP_GROUP_TEAM_ADMIN: {{ $v.oidc.teamAdminGroupID }} IDP_GROUP_MAPPINGS_TEAMS: '{{ $teamsMapping | toJson }}' - IDP_OIDC_URL: {{ $idp.issuer }} + IDP_OIDC_URL: {{ $v.oidc.issuer }} REDIRECT_URIS: '[ "https://otomi.{{ $v.cluster.domain }}", "https://auth.{{ $v.cluster.domain }}/*", diff --git a/values/k8s/k8s-raw.gotmpl b/values/k8s/k8s-raw.gotmpl index 64728a7e24..1ec385b5d8 100644 --- a/values/k8s/k8s-raw.gotmpl +++ b/values/k8s/k8s-raw.gotmpl @@ -11,7 +11,8 @@ resources: istio-injection: {{ if $ns | getOrNil "disableIstioInjection" }}disabled{{ else }}enabled{{ end }} {{- end }} {{- range $id, $team := $t.teams }} - {{- $ns := printf "team-%s" $id }} + {{- $ns := printf "team-%s" $id }} + {{- if has $v.cluster.id $team.clusters }} - apiVersion: v1 kind: Namespace metadata: @@ -19,24 +20,25 @@ resources: labels: name: {{ $ns }} istio-injection: enabled - {{- $pullSecrets := list }} - {{- range $s := $team.secrets }} - {{- if eq $s.type "docker-registry" }} - {{- $pullSecrets = (append $pullSecrets $s) }} - {{- end }} - {{- end }} + {{- $pullSecrets := list }} + {{- range $s := $team.secrets }} + {{- if eq $s.type "docker-registry" }} + {{- $pullSecrets = (append $pullSecrets $s) }} + {{- end }} + {{- end }} # patching service account here as helm does not recognize it as it's own - apiVersion: v1 kind: ServiceAccount metadata: name: default namespace: {{ $ns }} - {{- if gt (len $pullSecrets) 0 }} + {{- if gt (len $pullSecrets) 0 }} imagePullSecrets: - {{- range $s := $pullSecrets }} - - name: {{ $s.name }} - {{- end }} - {{- end }} + {{- range $s := $pullSecrets }} + - name: {{ $s.name }} + {{- end }} + {{- end }} + {{- end }} {{- end }} - apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/values/oauth2-proxy/oauth2-proxy.gotmpl b/values/oauth2-proxy/oauth2-proxy.gotmpl index a1c26595ee..c2002680b8 100644 --- a/values/oauth2-proxy/oauth2-proxy.gotmpl +++ b/values/oauth2-proxy/oauth2-proxy.gotmpl @@ -76,7 +76,7 @@ extraArgs: # Provider specifics: ### {{- if eq $provider "oidc" }} - oidc-issuer-url: {{ $hasKeycloak | ternary $keycloakIssuer $o.idp.issuer }} + oidc-issuer-url: {{ $hasKeycloak | ternary $keycloakIssuer $o.issuer }} insecure-oidc-allow-unverified-email: true {{- else if eq $provider "azure" }} azure-tenant: {{ $o.azure.tenantID }} diff --git a/values/otomi-api/otomi-api.gotmpl b/values/otomi-api/otomi-api.gotmpl index 17a467e98c..ec98344f4b 100644 --- a/values/otomi-api/otomi-api.gotmpl +++ b/values/otomi-api/otomi-api.gotmpl @@ -39,7 +39,7 @@ env: {{- end }} USE_SOPS: {{ $v.sops.enabled }} CORE_VERSION: '{{ $version }}' - {{- if (not $v.charts.keycloak | get "enabled" false) }} + {{- if (not ($v.charts.keycloak | get "enabled" false)) }} NO_AUTHZ: true {{- end }} diff --git a/values/prometheus-operator/prometheus-operator.gotmpl b/values/prometheus-operator/prometheus-operator.gotmpl index b384fd3458..0098e868ff 100644 --- a/values/prometheus-operator/prometheus-operator.gotmpl +++ b/values/prometheus-operator/prometheus-operator.gotmpl @@ -167,13 +167,15 @@ grafana: service: portName: http-service grafana.ini: + {{ if not $hasOIDC }} "auth.anonymous": - enabled: {{ not $hasOIDC }} + enabled: true org_role: Admin org_name: Main Org. + {{- else }} "auth.generic_oauth": tls_skip_verify_insecure: {{ eq ($v.charts | get "cert-manager.stage") "staging" }} - enabled: {{ $hasOIDC }} + enabled: true name: OAuth org_role: Admin allow_sign_up: true @@ -186,6 +188,7 @@ grafana: token_url: {{ $hasKeycloak | ternary (printf "%s/protocol/openid-connect/token" $keycloakBase) ($v.oidc | getOrNil "grafana.tokenUrl") }} api_url: {{ $hasKeycloak | ternary (printf "%s/protocol/openid-connect/userinfo" $keycloakBase) ($v.oidc | getOrNil "grafana.apiUrl") }} role_attribute_path: contains(groups[*], 'admin') && 'Admin' || contains(groups[*], 'team-admin') && 'Admin' || 'Editor' + {{- end }} log: level: error server: