Compare commits
No commits in common. "develop" and "v1.0.1" have entirely different histories.
|
@ -1,5 +0,0 @@
|
||||||
/bin
|
|
||||||
/dist
|
|
||||||
/tools
|
|
||||||
/.trivy
|
|
||||||
.mktools/
|
|
|
@ -56,7 +56,7 @@ before_deploy:
|
||||||
deploy:
|
deploy:
|
||||||
- provider: releases
|
- provider: releases
|
||||||
api_key:
|
api_key:
|
||||||
secure: X0diTlyWbycImd8x1ce1VHC9IcaPD9f0Pl9ynYHoV0BNw4KrgGD6OdiOM7Z7fiZA8jly0jh81orxdm3o7hIAlX02BA2kg8BFi83dcm01pcwW5vZZq0w/XMMtw644O8CAImPr58YKUjBb7c1+RENjPqjIZrdjVDzeRu0k5oOaDpk94016B1j1OB9XtkXzPzgP+KTx5gCmfeipQ6LxbL8wzXEqJWGF6+5B/7bqkzSw7vIvDtYlYgM7jf5NZslxiKlVr7pkRik6KlfRjtt/pi4ZJwpcu4AHdnNZoXkcmQwz21yd1lVFUbqNi8qGJyilOQB+p1RvZ/c5Q7a+FHN9anUM06DtfL/bMII+Kqt429M4sk9mA3rEp61SkDML1o9lJ8iiEKXPxMNtWKv1I/ixrZWYPE3g75fTaoyBusBS1bxNtOgAUf2dIr0JCO+p2NTaCyd0vOr87/KMVnJTeLzOnisMY0DOjQ3FhYmXSRPtyJF72jDxh338YX7hHK5DcwL9aVKOA5t7VbM2RW6VdavH5kHUPDEbZaDFlsUcncHJnzD1QtX7ODJh7EgFteE32fmJFBcE2BAWDIvYy68sCNU64Wwh2yBCbnMzLhET/nLm3Jj4U6Jl3VqAT2ff0UQBgugW0OpxO7JBW4b3dNwmKKA6WE5+WitO8EzE8sA09tnIEIe7YT8=
|
secure: f3KYUKsrtYRKcttPfWmHWGCFT2ugas1fgbBACGCTFp/ir6AAfHt16FYHgyfXc8+V9IajNkwqL6TrDjQFfLSI0qx38s+wLK6FIicjvMzVWXe/lCHByr4kAZuN2BOynrKiE8rkEcXHGjX2adrrvETNwUCp+oIxQtdIAVvE1Fjkb+MdnCs7Ed9beggqNOJksGVJ44X17ezarCc8/L4ULIXY/OBLnUnwH6UwMrSIPuwvMJAZlhyJWOv4ro8Z3D2f5vfD91MNit8rCkXkYPKnw1/rIpBbaoARLQ95bN97NsLkfeNlSgoAXhy00i+Jz78PgD3TvTPecdlTPwBNNnHaXBtQZjB+qHpr4lW/NP2+IJ6Aku8JY2X+Srd/BYD8hh4Nqzp3UiymsQS61++jfZmi3xUu5nhFkd+MavVW8Xy0/8vnREqHuwCQ8+oo1GHqDnKgdeRMm29AwTTx/FyUSPlzWzQIC1PVFtS3/YYqAn7sooS6l5MuSENk05IYOM1ApXOGb6tNW8wDGTD8QP8KvJjfARg8365wwhEAP6gdrW6VotSjY5XZM37ge0uKfKBvw8BVNfbn/R4/12KIuqPsEmbVfFJx18DQzz3b+9UfPZQwuxZvgNnngplUbzP2q/cKYNSMHKzZ53EVPPr5wtdDWm5pnbLtWbrN5d+y2FoS+YBrCrL09C8=
|
||||||
file:
|
file:
|
||||||
- bin/werther_linux_386.tar.gz
|
- bin/werther_linux_386.tar.gz
|
||||||
- bin/werther_linux_amd64.tar.gz
|
- bin/werther_linux_amd64.tar.gz
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# This source code is licensed under the MIT license found in the
|
# This source code is licensed under the MIT license found in the
|
||||||
# LICENSE file in the root directory of this source tree.
|
# LICENSE file in the root directory of this source tree.
|
||||||
|
|
||||||
FROM golang:1.21-alpine AS build
|
FROM golang:1.12-alpine AS build
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
ARG GOPROXY
|
ARG GOPROXY
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
@Library('cadoles') _
|
|
||||||
|
|
||||||
// Utilisation du pipeline "standard"
|
|
||||||
// Voir https://forge.cadoles.com/Cadoles/Jenkins/src/branch/master/doc/tutorials/standard-make-pipeline.md
|
|
||||||
standardMakePipeline([
|
|
||||||
'dockerfileExtension': '''
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y zip jq
|
|
||||||
|
|
||||||
RUN wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz \
|
|
||||||
&& rm -rf /usr/local/go \
|
|
||||||
&& tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
|
|
||||||
|
|
||||||
ENV PATH="${PATH}:/usr/local/go/bin"
|
|
||||||
''',
|
|
||||||
'hooks': [
|
|
||||||
'pre-release': {
|
|
||||||
// Login into docker registry
|
|
||||||
sh '''
|
|
||||||
make .mktools
|
|
||||||
echo "$MKT_GITEA_RELEASE_PASSWORD" | docker login --username "$MKT_GITEA_RELEASE_USERNAME" --password-stdin reg.cadoles.com
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Use credentials to push images to registry and pubish gitea release
|
|
||||||
'credentials': [
|
|
||||||
usernamePassword(credentialsId: 'kipp-credentials', usernameVariable: 'MKT_GITEA_RELEASE_USERNAME', passwordVariable: 'MKT_GITEA_RELEASE_PASSWORD')
|
|
||||||
]
|
|
||||||
])
|
|
86
Makefile
86
Makefile
|
@ -1,86 +0,0 @@
|
||||||
SHELL := /bin/bash
|
|
||||||
|
|
||||||
IMAGE_NAME := reg.cadoles.com/cadoles/hydra-werther
|
|
||||||
|
|
||||||
NFPM_VERSION ?= 2.20.0
|
|
||||||
NFPM_PACKAGERS ?= deb rpm
|
|
||||||
|
|
||||||
MKT_GITEA_RELEASE_ORG ?= Cadoles
|
|
||||||
MKT_GITEA_RELEASE_PROJECT ?= hydra-werther
|
|
||||||
MKT_GITEA_RELEASE_VERSION ?= $(MKT_PROJECT_VERSION)
|
|
||||||
|
|
||||||
define MKT_GITEA_RELEASE_BODY
|
|
||||||
## Docker usage
|
|
||||||
|
|
||||||
```
|
|
||||||
docker pull $(IMAGE_NAME):$(MKT_PROJECT_VERSION)
|
|
||||||
```
|
|
||||||
endef
|
|
||||||
export MKT_GITEA_RELEASE_BODY
|
|
||||||
|
|
||||||
build: build-bin build-image
|
|
||||||
|
|
||||||
build-bin: clean generate
|
|
||||||
CGO_ENABLED=0 misc/script/build
|
|
||||||
|
|
||||||
test: scan
|
|
||||||
|
|
||||||
generate:
|
|
||||||
go generate ./...
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf bin dist
|
|
||||||
|
|
||||||
dist:
|
|
||||||
mkdir -p dist
|
|
||||||
|
|
||||||
package: clean build-bin $(foreach p,$(NFPM_PACKAGERS), package-$(p))
|
|
||||||
|
|
||||||
package-%: dist tools/nfpm/bin/nfpm
|
|
||||||
PACKAGE_VERSION=$(MKT_PROJECT_VERSION) \
|
|
||||||
tools/nfpm/bin/nfpm package \
|
|
||||||
--config misc/packaging/nfpm.yml \
|
|
||||||
--target ./dist \
|
|
||||||
--packager $*
|
|
||||||
|
|
||||||
tools/nfpm/bin/nfpm:
|
|
||||||
mkdir -p tools/nfpm/bin
|
|
||||||
curl -L --output tools/nfpm/nfpm_$(NFPM_VERSION)_Linux_x86_64.tar.gz https://github.com/goreleaser/nfpm/releases/download/v$(NFPM_VERSION)/nfpm_$(NFPM_VERSION)_Linux_x86_64.tar.gz \
|
|
||||||
&& tar -xzf tools/nfpm/nfpm_$(NFPM_VERSION)_Linux_x86_64.tar.gz -C tools/nfpm/bin \
|
|
||||||
&& chmod +x tools/nfpm/bin/nfpm \
|
|
||||||
&& rm -f tools/nfpm/nfpm_$(NFPM_VERSION)_Linux_x86_64.tar.gz
|
|
||||||
|
|
||||||
build-image:
|
|
||||||
docker build \
|
|
||||||
-t "${IMAGE_NAME}:latest" \
|
|
||||||
.
|
|
||||||
|
|
||||||
scan: build-image tools/trivy/bin/trivy
|
|
||||||
mkdir -p .trivy
|
|
||||||
tools/trivy/bin/trivy --cache-dir .trivy/.cache image --ignorefile .trivyignore.yaml $(TRIVY_ARGS) $(IMAGE_NAME):latest
|
|
||||||
|
|
||||||
tools/trivy/bin/trivy:
|
|
||||||
mkdir -p tools/trivy/bin
|
|
||||||
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b ./tools/trivy/bin v0.47.0
|
|
||||||
|
|
||||||
release: release-image release-gitea
|
|
||||||
|
|
||||||
release-gitea: .mktools package
|
|
||||||
@[ ! -z "$(MKT_PROJECT_VERSION)" ] || ( echo "Just downloaded mktools. Please re-run command."; exit 1 )
|
|
||||||
$(MAKE) MKT_GITEA_RELEASE_ATTACHMENTS="$$(find dist/* -type f -printf '%p ')" mkt-gitea-release
|
|
||||||
|
|
||||||
release-image: .mktools
|
|
||||||
@[ ! -z "$(MKT_PROJECT_VERSION)" ] || ( echo "Just downloaded mktools. Please re-run command."; exit 1 )
|
|
||||||
docker tag "${IMAGE_NAME}:latest" "${IMAGE_NAME}:$(MKT_PROJECT_VERSION)"
|
|
||||||
docker tag "${IMAGE_NAME}:latest" "${IMAGE_NAME}:$(MKT_PROJECT_SHORT_VERSION)"
|
|
||||||
docker tag "${IMAGE_NAME}:latest" "${IMAGE_NAME}:$(MKT_PROJECT_VERSION_CHANNEL)-latest"
|
|
||||||
|
|
||||||
docker push "${IMAGE_NAME}:$(MKT_PROJECT_VERSION)"
|
|
||||||
docker push "${IMAGE_NAME}:$(MKT_PROJECT_SHORT_VERSION)"
|
|
||||||
docker push "${IMAGE_NAME}:$(MKT_PROJECT_VERSION_CHANNEL)-latest"
|
|
||||||
|
|
||||||
.mktools:
|
|
||||||
rm -rf .mktools
|
|
||||||
curl -q https://forge.cadoles.com/Cadoles/mktools/raw/branch/master/install.sh | TASKS="version gitea" $(SHELL)
|
|
||||||
|
|
||||||
-include .mktools/*.mk
|
|
105
README.md
105
README.md
|
@ -166,96 +166,13 @@ After that you should set the directory path to the environment variable `WERTHE
|
||||||
|
|
||||||
### Custom login page
|
### Custom login page
|
||||||
|
|
||||||
A login page's template must be a Go template. The template has access to data conforming the next JSON-schema:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
- WebBasePath:
|
|
||||||
description: The base path of the login page
|
|
||||||
type: string
|
|
||||||
- LangPrefs:
|
|
||||||
description: The user language preferences (the parsed value of the header Accept-Language)
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
- Lang:
|
|
||||||
description: The language canonical name.
|
|
||||||
type: string
|
|
||||||
- Weight:
|
|
||||||
description: The language weight.
|
|
||||||
type: number
|
|
||||||
required:
|
|
||||||
- Lang
|
|
||||||
- Weight
|
|
||||||
- Data:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
- CSRFToken:
|
|
||||||
description: A CSRF token.
|
|
||||||
type: string
|
|
||||||
- Challenge:
|
|
||||||
description: A login challenge ID.
|
|
||||||
type: string
|
|
||||||
- LoginURL:
|
|
||||||
description: An endpoint that finishes the login process.
|
|
||||||
type: string
|
|
||||||
- IsInvalidCredentials:
|
|
||||||
description: Specifies that a user types an invalid username or password.
|
|
||||||
type: boolean
|
|
||||||
- IsInternalError:
|
|
||||||
description: Specifies that an internal server error happens when finishing the login process.
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- CSRFToken
|
|
||||||
- Challenge
|
|
||||||
- LoginURL
|
|
||||||
- IsInvalidCredentials
|
|
||||||
- IsInternalError
|
|
||||||
required:
|
|
||||||
- WebBasePath
|
|
||||||
- LangPrefs
|
|
||||||
- Data
|
|
||||||
```
|
|
||||||
|
|
||||||
When a login page's template contains static resources (like styles, scripts, and images)
|
|
||||||
they must be placed in a subdirectory called `static`.
|
|
||||||
|
|
||||||
For a full example of a login page's template see [source code](internal/web/templates).
|
|
||||||
|
|
||||||
### Custom login page (old format)
|
|
||||||
|
|
||||||
*The old template format is also supported but it will be removed in the future major release.*
|
|
||||||
|
|
||||||
A login page's template should contains blocks `title`, `style`, `script`, `content`.
|
A login page's template should contains blocks `title`, `style`, `script`, `content`.
|
||||||
Each block has access to data conforming the next JSON-schema:
|
Each block has access to data that is an object with the next properties:
|
||||||
|
- `CSRFToken` (string) - a CSRF token;
|
||||||
```yaml
|
- `Challenge` (string) - a login challenge ID;
|
||||||
type: object
|
- `LoginURL` (string) - an endpoint that finishes the login process;
|
||||||
properties:
|
- `IsInvalidCredentials` (bool) - specifies that a user types an invalid username or password;
|
||||||
- CSRFToken:
|
- `IsInternalError` (bool) specifies that an internal server error happens when finishing the login process.
|
||||||
description: A CSRF token.
|
|
||||||
type: string
|
|
||||||
- Challenge:
|
|
||||||
description: A login challenge ID.
|
|
||||||
type: string
|
|
||||||
- LoginURL:
|
|
||||||
description: An endpoint that finishes the login process.
|
|
||||||
type: string
|
|
||||||
- IsInvalidCredentials:
|
|
||||||
description: Specifies that a user types an invalid username or password.
|
|
||||||
type: boolean
|
|
||||||
- IsInternalError:
|
|
||||||
description: Specifies that an internal server error happens when finishing the login process.
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- CSRFToken
|
|
||||||
- Challenge
|
|
||||||
- LoginURL
|
|
||||||
- IsInvalidCredentials
|
|
||||||
- IsInternalError
|
|
||||||
```
|
|
||||||
|
|
||||||
When a login page's template contains static resources (like styles, scripts, and images)
|
When a login page's template contains static resources (like styles, scripts, and images)
|
||||||
they must be placed in a subdirectory called `static`.
|
they must be placed in a subdirectory called `static`.
|
||||||
|
@ -313,7 +230,7 @@ For a full example of a login page's template see [source code](internal/web/tem
|
||||||
- --grant-types
|
- --grant-types
|
||||||
- implicit
|
- implicit
|
||||||
- --scope
|
- --scope
|
||||||
- openid,profile,email,roles
|
- openid,profile,email
|
||||||
- --callbacks
|
- --callbacks
|
||||||
- http://localhost:3000
|
- http://localhost:3000
|
||||||
- --post-logout-callbacks
|
- --post-logout-callbacks
|
||||||
|
@ -338,8 +255,8 @@ For a full example of a login page's template see [source code](internal/web/tem
|
||||||
URLS_LOGIN: http://localhost:8080/auth/login
|
URLS_LOGIN: http://localhost:8080/auth/login
|
||||||
URLS_CONSENT: http://localhost:8080/auth/consent
|
URLS_CONSENT: http://localhost:8080/auth/consent
|
||||||
URLS_LOGOUT: http://localhost:8080/auth/logout
|
URLS_LOGOUT: http://localhost:8080/auth/logout
|
||||||
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_SCOPES: profile,email,phone,roles
|
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_SCOPES: profile,email,phone
|
||||||
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_CLAIMS: name,family_name,given_name,nickname,email,phone_number,https://github.com/i-core/werther/claims/roles
|
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_CLAIMS: name,family_name,given_name,nickname,email,phone_number
|
||||||
DSN: memory
|
DSN: memory
|
||||||
command: serve all --dangerous-force-http
|
command: serve all --dangerous-force-http
|
||||||
networks:
|
networks:
|
||||||
|
@ -353,7 +270,7 @@ For a full example of a login page's template see [source code](internal/web/tem
|
||||||
depends_on:
|
depends_on:
|
||||||
- werther
|
- werther
|
||||||
werther:
|
werther:
|
||||||
image: icoreru/werther:v1.1.1
|
image: icoreru/werther:v1.0.0
|
||||||
environment:
|
environment:
|
||||||
WERTHER_IDENTP_HYDRA_URL: http://hydra:4445
|
WERTHER_IDENTP_HYDRA_URL: http://hydra:4445
|
||||||
WERTHER_LDAP_ENDPOINTS: ldap:389
|
WERTHER_LDAP_ENDPOINTS: ldap:389
|
||||||
|
@ -390,7 +307,7 @@ For a full example of a login page's template see [source code](internal/web/tem
|
||||||
docker stack deploy -c docker-compose.yml auth
|
docker stack deploy -c docker-compose.yml auth
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Open the browser with http://localhost:4444/oauth2/auth?client_id=test-client&response_type=token&scope=openid%20profile%20email%20roles&state=12345678.
|
4. Open the browser with http://localhost:4444/oauth2/auth?client_id=test-client&response_type=token&scope=openid%20profile%20email&state=12345678.
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,6 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"github.com/i-core/rlog"
|
"github.com/i-core/rlog"
|
||||||
"github.com/i-core/routegroup"
|
"github.com/i-core/routegroup"
|
||||||
"github.com/i-core/werther/internal/identp"
|
"github.com/i-core/werther/internal/identp"
|
||||||
|
@ -32,9 +30,8 @@ var version = ""
|
||||||
|
|
||||||
// Config is a server's configuration.
|
// Config is a server's configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DevMode bool `envconfig:"dev_mode" default:"false" desc:"Enable development mode"`
|
DevMode bool `envconfig:"dev_mode" default:"false" desc:"a development mode"`
|
||||||
Listen string `default:":8080" desc:"a host and port to listen on (<host>:<port>)"`
|
Listen string `default:":8080" desc:"a host and port to listen on (<host>:<port>)"`
|
||||||
InsecureSkipVerify bool `envconfig:"insecure_skip_verify" default:"false" desc:"Disable TLS verification on Hydra connection"`
|
|
||||||
Identp identp.Config
|
Identp identp.Config
|
||||||
LDAP ldapclient.Config
|
LDAP ldapclient.Config
|
||||||
Web web.Config
|
Web web.Config
|
||||||
|
@ -83,11 +80,6 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cnf.InsecureSkipVerify {
|
|
||||||
log.Warn("All ssl verifications are disabled !")
|
|
||||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
ldap := ldapclient.New(cnf.LDAP)
|
ldap := ldapclient.New(cnf.LDAP)
|
||||||
|
|
||||||
router := routegroup.NewRouter(nosurf.NewPure, rlog.NewMiddleware(log))
|
router := routegroup.NewRouter(nosurf.NewPure, rlog.NewMiddleware(log))
|
||||||
|
|
|
@ -1,131 +0,0 @@
|
||||||
#WERTHER_DEV_MODE=
|
|
||||||
# [description] a development mode
|
|
||||||
# [type] True or False
|
|
||||||
# [default] false
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
#WERTHER_LISTEN=
|
|
||||||
# [description] a host and port to listen on (<host>:<port>)
|
|
||||||
# [type] String
|
|
||||||
# [default] :8080
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
WERTHER_IDENTP_HYDRA_URL=http://localhost:4445/
|
|
||||||
# [description] an admin URL of ORY Hydra Server
|
|
||||||
# [type] String
|
|
||||||
# [default]
|
|
||||||
# [required] true
|
|
||||||
|
|
||||||
#WERTHER_IDENTP_SESSION_TTL=
|
|
||||||
# [description] a user session's TTL
|
|
||||||
# [type] Duration
|
|
||||||
# [default] 24h
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
#WERTHER_IDENTP_CLAIM_SCOPES=
|
|
||||||
# [description] a mapping of OpenID Connect claims to scopes (all claims are URL encoded)
|
|
||||||
# [type] Comma-separated list of String:String pairs
|
|
||||||
# [default] name:profile,family_name:profile,given_name:profile,email:email,https%3A%2F%2Fgithub.com%2Fi-core%2Fwerther%2Fclaims%2Froles:roles
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
WERTHER_LDAP_ENDPOINTS=localhost:389
|
|
||||||
# [description] a LDAP's server URLs as "<address>:<port>"
|
|
||||||
# [type] Comma-separated list of String
|
|
||||||
# [default]
|
|
||||||
# [required] true
|
|
||||||
|
|
||||||
WERTHER_LDAP_BINDDN=
|
|
||||||
# [description] a LDAP bind DN
|
|
||||||
# [type] String
|
|
||||||
# [default]
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
WERTHER_LDAP_BINDPW=
|
|
||||||
# [description] a LDAP bind password
|
|
||||||
# [type] String
|
|
||||||
# [default]
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
WERTHER_LDAP_BASEDN=ou=users,dc=myorg,dc=com
|
|
||||||
# [description] a LDAP base DN for searching users
|
|
||||||
# [type] String
|
|
||||||
# [default]
|
|
||||||
# [required] true
|
|
||||||
|
|
||||||
#WERTHER_LDAP_USER_SEARCH_QUERY=
|
|
||||||
# [description] the user search query
|
|
||||||
# [type] String
|
|
||||||
# [default] (&(|(objectClass=organizationalPerson)(objectClass=inetOrgPerson))(|(uid=%[1]s)(mail=%[1]s)(userPrincipalName=%[1]s)(sAMAccountName=%[1]s)))
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
#WERTHER_LDAP_ATTR_CLAIMS=
|
|
||||||
# [description] a mapping of LDAP attributes to OpenID connect claims
|
|
||||||
# [type] Comma-separated list of String:String pairs
|
|
||||||
# [default] name:name,sn:family_name,givenName:given_name,mail:email
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
WERTHER_LDAP_ROLE_BASEDN=ou=groups,dc=myorg,dc=com
|
|
||||||
# [description] a LDAP base DN for searching roles
|
|
||||||
# [type] String
|
|
||||||
# [default]
|
|
||||||
# [required] true
|
|
||||||
|
|
||||||
#WERTHER_LDAP_ROLE_SEARCH_QUERY=
|
|
||||||
# [description] the role search query
|
|
||||||
# [type] String
|
|
||||||
# [default] (|(&(|(objectClass=group)(objectClass=groupOfNames))(member=%[1]s))(&(objectClass=groupOfUniqueNames)(uniqueMember=%[1]s)))
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
#WERTHER_LDAP_ROLE_ATTR=
|
|
||||||
# [description] a LDAP group's attribute that contains a role's name
|
|
||||||
# [type] String
|
|
||||||
# [default] description
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
#WERTHER_LDAP_ROLE_CLAIM=
|
|
||||||
# [description] a name of an OpenID Connect claim that contains user roles
|
|
||||||
# [type] String
|
|
||||||
# [default] https://github.com/i-core/werther/claims/roles
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
#WERTHER_LDAP_CACHE_SIZE=
|
|
||||||
# [description] a user info cache's size in KiB
|
|
||||||
# [type] Integer
|
|
||||||
# [default] 512
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
#WERTHER_LDAP_CACHE_TTL=
|
|
||||||
# [description] a user info cache TTL
|
|
||||||
# [type] Duration
|
|
||||||
# [default] 30m
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
#WERTHER_LDAP_IS_TLS=
|
|
||||||
# [description] should LDAP connection be established via TLS
|
|
||||||
# [type] True or False
|
|
||||||
# [default] false
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
#WERTHER_WEB_DIR=
|
|
||||||
# [description] a path to an external web directory
|
|
||||||
# [type] String
|
|
||||||
# [default]
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
#WERTHER_WEB_BASE_PATH=
|
|
||||||
# [description] a base path of web pages
|
|
||||||
# [type] String
|
|
||||||
# [default] /
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
#WERTHER_LDAP_CONNECTION_TIMEOUT=
|
|
||||||
# [description] LDAP server connection timeout
|
|
||||||
# [type] Duration
|
|
||||||
# [default] 60s
|
|
||||||
# [required]
|
|
||||||
|
|
||||||
# WERTHER_INSECURE_SKIP_VERIFY=
|
|
||||||
# [description] Disable TLS verification on Hydra connection
|
|
||||||
# [type] True or False
|
|
||||||
# [default] false
|
|
||||||
# [required]
|
|
|
@ -1 +0,0 @@
|
||||||
9
|
|
|
@ -1,14 +0,0 @@
|
||||||
Source: hydra-werther
|
|
||||||
Section: unknown
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Cadoles <contact@cadoles.com>
|
|
||||||
Build-Depends: debhelper (>= 8.0.0), wget, ca-certificates, tar
|
|
||||||
Standards-Version: 3.9.4
|
|
||||||
Homepage: http://forge.cadoles.com/Cadoles/hydra-werther
|
|
||||||
Vcs-Git: http://forge.cadoles.com/Cadoles/hydra-werther.git
|
|
||||||
Vcs-Browser: http://forge.cadoles.com/Cadoles/hydra-werther
|
|
||||||
|
|
||||||
Package: hydra-werther
|
|
||||||
Architecture: amd64
|
|
||||||
Depends: ${shlibs:Depends}, ${misc:Depends}, ssl-cert
|
|
||||||
Description: Hydra identity provider backed by LDAP compatible server
|
|
|
@ -1,12 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Run Hydra Werther login/consent/logout app
|
|
||||||
After=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
EnvironmentFile=/etc/hydra-werther/hydra-werther.conf
|
|
||||||
ExecStart=/usr/bin/hydra-werther
|
|
||||||
Restart=on-failure
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
|
@ -1,40 +0,0 @@
|
||||||
#!/usr/bin/make -f
|
|
||||||
# -*- makefile -*-
|
|
||||||
|
|
||||||
# Uncomment this to turn on verbose mode.
|
|
||||||
export DH_VERBOSE=1
|
|
||||||
|
|
||||||
GO_VERSION := 1.17.1
|
|
||||||
OS := linux
|
|
||||||
ARCH := amd64
|
|
||||||
GOPATH=$(HOME)/go
|
|
||||||
|
|
||||||
ifeq (, $(shell which go 2>/dev/null))
|
|
||||||
override_dh_auto_build: install-go
|
|
||||||
endif
|
|
||||||
|
|
||||||
%:
|
|
||||||
dh $@ --with systemd
|
|
||||||
|
|
||||||
install-go:
|
|
||||||
wget https://dl.google.com/go/go$(GO_VERSION).$(OS)-$(ARCH).tar.gz
|
|
||||||
tar -C /usr/local -xzf go$(GO_VERSION).$(OS)-$(ARCH).tar.gz
|
|
||||||
|
|
||||||
override_dh_auto_build: $(GOPATH)
|
|
||||||
GOPATH=$(GOPATH) PATH="$(PATH):/usr/local/go/bin:$(GOPATH)/bin" DISTS=$(OS)/$(ARCH) make
|
|
||||||
|
|
||||||
$(GOPATH):
|
|
||||||
mkdir -p $(GOPATH)
|
|
||||||
|
|
||||||
override_dh_auto_install:
|
|
||||||
mkdir -p debian/hydra-werther/usr/bin
|
|
||||||
mkdir -p debian/hydra-werther/etc/hydra-werther
|
|
||||||
|
|
||||||
cp bin/werther_$(OS)_$(ARCH) debian/hydra-werther/usr/bin/hydra-werther
|
|
||||||
cp conf/hydra-werther.conf debian/hydra-werther/etc/hydra-werther
|
|
||||||
|
|
||||||
install -d debian/hydra-werther
|
|
||||||
|
|
||||||
override_dh_strip:
|
|
||||||
|
|
||||||
override_dh_auto_test:
|
|
|
@ -1 +0,0 @@
|
||||||
3.0 (native)
|
|
25
go.mod
25
go.mod
|
@ -1,34 +1,21 @@
|
||||||
module github.com/i-core/werther
|
module github.com/i-core/werther
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2 // indirect
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
|
||||||
|
github.com/cespare/xxhash v1.0.0 // indirect
|
||||||
github.com/coocood/freecache v1.0.1
|
github.com/coocood/freecache v1.0.1
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.0
|
github.com/elazarl/go-bindata-assetfs v1.0.0
|
||||||
github.com/go-ldap/ldap/v3 v3.2.3
|
|
||||||
github.com/i-core/rlog v1.0.0
|
github.com/i-core/rlog v1.0.0
|
||||||
github.com/i-core/routegroup v1.0.0
|
github.com/i-core/routegroup v1.0.0
|
||||||
github.com/justinas/nosurf v0.0.0-20171023064657-7182011986c4
|
github.com/justinas/nosurf v0.0.0-20171023064657-7182011986c4
|
||||||
github.com/kelseyhightower/envconfig v1.3.0
|
github.com/kelseyhightower/envconfig v1.3.0
|
||||||
github.com/kevinburke/go-bindata v3.13.0+incompatible
|
github.com/kevinburke/go-bindata v3.13.0+incompatible
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
go.uber.org/zap v1.10.0
|
|
||||||
golang.org/x/text v0.3.2
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
|
|
||||||
github.com/OneOfOne/xxhash v1.2.2 // indirect
|
|
||||||
github.com/cespare/xxhash v1.0.0 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
|
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible // indirect
|
|
||||||
github.com/julienschmidt/httprouter v1.2.0 // indirect
|
|
||||||
github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da // indirect
|
|
||||||
github.com/sergi/go-diff v1.0.0 // indirect
|
github.com/sergi/go-diff v1.0.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 // indirect
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 // indirect
|
||||||
go.uber.org/atomic v1.4.0 // indirect
|
go.uber.org/zap v1.10.0
|
||||||
go.uber.org/multierr v1.1.0 // indirect
|
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect
|
gopkg.in/ldap.v2 v2.5.1
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.21
|
|
||||||
|
|
20
go.sum
20
go.sum
|
@ -1,5 +1,3 @@
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
|
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
|
||||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
|
@ -13,10 +11,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
|
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
|
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
|
||||||
github.com/go-ldap/ldap/v3 v3.2.3 h1:FBt+5w3q/vPVPb4eYMQSn+pOiz4zewPamYhlGMmc7yM=
|
|
||||||
github.com/go-ldap/ldap/v3 v3.2.3/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
|
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/i-core/rlog v1.0.0 h1:8CY2rsqvm3Z9cfl3hroppn8LTBwbtL45+ho79JTz8Jg=
|
github.com/i-core/rlog v1.0.0 h1:8CY2rsqvm3Z9cfl3hroppn8LTBwbtL45+ho79JTz8Jg=
|
||||||
|
@ -50,13 +44,7 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225 h1:JBwmEvLfCqgPcIq8MjVMQxsF3LVL4XG/HH0qiG0+IFY=
|
||||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
|
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
|
||||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
|
|
|
@ -14,18 +14,17 @@ import (
|
||||||
// ConsentReqDoer fetches information on the OAuth2 request and then accept or reject the requested authentication process.
|
// ConsentReqDoer fetches information on the OAuth2 request and then accept or reject the requested authentication process.
|
||||||
type ConsentReqDoer struct {
|
type ConsentReqDoer struct {
|
||||||
hydraURL string
|
hydraURL string
|
||||||
fakeTLSTermination bool
|
|
||||||
rememberFor int
|
rememberFor int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConsentReqDoer creates a ConsentRequest.
|
// NewConsentReqDoer creates a ConsentRequest.
|
||||||
func NewConsentReqDoer(hydraURL string, fakeTLSTermination bool, rememberFor int) *ConsentReqDoer {
|
func NewConsentReqDoer(hydraURL string, rememberFor int) *ConsentReqDoer {
|
||||||
return &ConsentReqDoer{hydraURL: hydraURL, fakeTLSTermination: fakeTLSTermination, rememberFor: rememberFor}
|
return &ConsentReqDoer{hydraURL: hydraURL, rememberFor: rememberFor}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitiateRequest fetches information on the OAuth2 request.
|
// InitiateRequest fetches information on the OAuth2 request.
|
||||||
func (crd *ConsentReqDoer) InitiateRequest(challenge string) (*ReqInfo, error) {
|
func (crd *ConsentReqDoer) InitiateRequest(challenge string) (*ReqInfo, error) {
|
||||||
ri, err := initiateRequest(consent, crd.hydraURL, crd.fakeTLSTermination, challenge)
|
ri, err := initiateRequest(consent, crd.hydraURL, challenge)
|
||||||
return ri, errors.Wrap(err, "failed to initiate consent request")
|
return ri, errors.Wrap(err, "failed to initiate consent request")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +46,6 @@ func (crd *ConsentReqDoer) AcceptConsentRequest(challenge string, remember bool,
|
||||||
IDToken: idToken,
|
IDToken: idToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
redirectURI, err := acceptRequest(consent, crd.hydraURL, crd.fakeTLSTermination, challenge, data)
|
redirectURI, err := acceptRequest(consent, crd.hydraURL, challenge, data)
|
||||||
return redirectURI, errors.Wrap(err, "failed to accept consent request")
|
return redirectURI, errors.Wrap(err, "failed to accept consent request")
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ func TestInitiateConsentRequest(t *testing.T) {
|
||||||
h := &testInitiateConsentHandler{reqInfo: tc.reqInfo, status: tc.status}
|
h := &testInitiateConsentHandler{reqInfo: tc.reqInfo, status: tc.status}
|
||||||
srv := httptest.NewServer(h)
|
srv := httptest.NewServer(h)
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
ldr := hydra.NewConsentReqDoer(srv.URL, false, tc.rememberFor)
|
ldr := hydra.NewConsentReqDoer(srv.URL, tc.rememberFor)
|
||||||
|
|
||||||
reqInfo, err := ldr.InitiateRequest(tc.challenge)
|
reqInfo, err := ldr.InitiateRequest(tc.challenge)
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ func TestAcceptConsentRequest(t *testing.T) {
|
||||||
h := &testAcceptConsentHandler{challenge: tc.challenge, status: tc.status, redirect: tc.redirect}
|
h := &testAcceptConsentHandler{challenge: tc.challenge, status: tc.status, redirect: tc.redirect}
|
||||||
srv := httptest.NewServer(h)
|
srv := httptest.NewServer(h)
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
ldr := hydra.NewConsentReqDoer(srv.URL, false, tc.rememberFor)
|
ldr := hydra.NewConsentReqDoer(srv.URL, tc.rememberFor)
|
||||||
|
|
||||||
var grantScope []string
|
var grantScope []string
|
||||||
for _, v := range tc.grantScope {
|
for _, v := range tc.grantScope {
|
||||||
|
|
|
@ -26,8 +26,6 @@ var (
|
||||||
ErrChallengeNotFound = errors.New("challenge not found")
|
ErrChallengeNotFound = errors.New("challenge not found")
|
||||||
// ErrChallengeExpired is an error that happens when a challenge is already used.
|
// ErrChallengeExpired is an error that happens when a challenge is already used.
|
||||||
ErrChallengeExpired = errors.New("challenge expired")
|
ErrChallengeExpired = errors.New("challenge expired")
|
||||||
//ErrServiceUnavailable is an error that happens when the hydra admin service is unavailable
|
|
||||||
ErrServiceUnavailable = errors.New("hydra service unavailable")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type reqType string
|
type reqType string
|
||||||
|
@ -46,7 +44,7 @@ type ReqInfo struct {
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func initiateRequest(typ reqType, hydraURL string, fakeTLSTermination bool, challenge string) (*ReqInfo, error) {
|
func initiateRequest(typ reqType, hydraURL, challenge string) (*ReqInfo, error) {
|
||||||
if challenge == "" {
|
if challenge == "" {
|
||||||
return nil, ErrChallengeMissed
|
return nil, ErrChallengeMissed
|
||||||
}
|
}
|
||||||
|
@ -54,23 +52,13 @@ func initiateRequest(typ reqType, hydraURL string, fakeTLSTermination bool, chal
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := parseURL(hydraURL)
|
u, err := parseURL(hydraURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
u = u.ResolveReference(ref)
|
u = u.ResolveReference(ref)
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", u.String(), nil)
|
resp, err := http.Get(u.String())
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if fakeTLSTermination {
|
|
||||||
req.Header.Add("X-Forwarded-Proto", "https")
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -88,7 +76,7 @@ func initiateRequest(typ reqType, hydraURL string, fakeTLSTermination bool, chal
|
||||||
return &ri, nil
|
return &ri, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptRequest(typ reqType, hydraURL string, fakeTLSTermination bool, challenge string, data interface{}) (string, error) {
|
func acceptRequest(typ reqType, hydraURL, challenge string, data interface{}) (string, error) {
|
||||||
if challenge == "" {
|
if challenge == "" {
|
||||||
return "", ErrChallengeMissed
|
return "", ErrChallengeMissed
|
||||||
}
|
}
|
||||||
|
@ -113,10 +101,6 @@ func acceptRequest(typ reqType, hydraURL string, fakeTLSTermination bool, challe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if fakeTLSTermination {
|
|
||||||
r.Header.Add("X-Forwarded-Proto", "https")
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Header.Set("Content-Type", "application/json")
|
r.Header.Set("Content-Type", "application/json")
|
||||||
resp, err := http.DefaultClient.Do(r)
|
resp, err := http.DefaultClient.Do(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -148,8 +132,6 @@ func checkResponse(resp *http.Response) error {
|
||||||
return ErrChallengeNotFound
|
return ErrChallengeNotFound
|
||||||
case 409:
|
case 409:
|
||||||
return ErrChallengeExpired
|
return ErrChallengeExpired
|
||||||
case 503:
|
|
||||||
return ErrServiceUnavailable
|
|
||||||
default:
|
default:
|
||||||
var rs struct {
|
var rs struct {
|
||||||
Message string `json:"error"`
|
Message string `json:"error"`
|
||||||
|
|
|
@ -14,18 +14,17 @@ import (
|
||||||
// LoginReqDoer fetches information on the OAuth2 request and then accept or reject the requested authentication process.
|
// LoginReqDoer fetches information on the OAuth2 request and then accept or reject the requested authentication process.
|
||||||
type LoginReqDoer struct {
|
type LoginReqDoer struct {
|
||||||
hydraURL string
|
hydraURL string
|
||||||
fakeTLSTermination bool
|
|
||||||
rememberFor int
|
rememberFor int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLoginReqDoer creates a LoginRequest.
|
// NewLoginReqDoer creates a LoginRequest.
|
||||||
func NewLoginReqDoer(hydraURL string, fakeTLSTermination bool, rememberFor int) *LoginReqDoer {
|
func NewLoginReqDoer(hydraURL string, rememberFor int) *LoginReqDoer {
|
||||||
return &LoginReqDoer{hydraURL: hydraURL, fakeTLSTermination: fakeTLSTermination, rememberFor: rememberFor}
|
return &LoginReqDoer{hydraURL: hydraURL, rememberFor: rememberFor}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitiateRequest fetches information on the OAuth2 request.
|
// InitiateRequest fetches information on the OAuth2 request.
|
||||||
func (lrd *LoginReqDoer) InitiateRequest(challenge string) (*ReqInfo, error) {
|
func (lrd *LoginReqDoer) InitiateRequest(challenge string) (*ReqInfo, error) {
|
||||||
ri, err := initiateRequest(login, lrd.hydraURL, lrd.fakeTLSTermination, challenge)
|
ri, err := initiateRequest(login, lrd.hydraURL, challenge)
|
||||||
return ri, errors.Wrap(err, "failed to initiate login request")
|
return ri, errors.Wrap(err, "failed to initiate login request")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +39,6 @@ func (lrd *LoginReqDoer) AcceptLoginRequest(challenge string, remember bool, sub
|
||||||
RememberFor: lrd.rememberFor,
|
RememberFor: lrd.rememberFor,
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
}
|
}
|
||||||
redirectURI, err := acceptRequest(login, lrd.hydraURL, lrd.fakeTLSTermination, challenge, data)
|
redirectURI, err := acceptRequest(login, lrd.hydraURL, challenge, data)
|
||||||
return redirectURI, errors.Wrap(err, "failed to accept login request")
|
return redirectURI, errors.Wrap(err, "failed to accept login request")
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ func TestInitiateLoginRequest(t *testing.T) {
|
||||||
h := &testInitiateLoginHandler{reqInfo: tc.reqInfo, status: tc.status}
|
h := &testInitiateLoginHandler{reqInfo: tc.reqInfo, status: tc.status}
|
||||||
srv := httptest.NewServer(h)
|
srv := httptest.NewServer(h)
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
ldr := hydra.NewLoginReqDoer(srv.URL, false, 0)
|
ldr := hydra.NewLoginReqDoer(srv.URL, 0)
|
||||||
|
|
||||||
reqInfo, err := ldr.InitiateRequest(tc.challenge)
|
reqInfo, err := ldr.InitiateRequest(tc.challenge)
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ func TestAcceptLoginRequest(t *testing.T) {
|
||||||
h := &testAcceptLoginHandler{challenge: tc.challenge, status: tc.status, redirect: tc.redirect}
|
h := &testAcceptLoginHandler{challenge: tc.challenge, status: tc.status, redirect: tc.redirect}
|
||||||
srv := httptest.NewServer(h)
|
srv := httptest.NewServer(h)
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
ldr := hydra.NewLoginReqDoer(srv.URL, false, tc.rememberFor)
|
ldr := hydra.NewLoginReqDoer(srv.URL, tc.rememberFor)
|
||||||
|
|
||||||
redirect, err := ldr.AcceptLoginRequest(tc.challenge, tc.remember, tc.subject)
|
redirect, err := ldr.AcceptLoginRequest(tc.challenge, tc.remember, tc.subject)
|
||||||
|
|
||||||
|
|
|
@ -14,22 +14,21 @@ import (
|
||||||
// LogoutReqDoer fetches information on the OAuth2 request and then accepts or rejects the requested logout process.
|
// LogoutReqDoer fetches information on the OAuth2 request and then accepts or rejects the requested logout process.
|
||||||
type LogoutReqDoer struct {
|
type LogoutReqDoer struct {
|
||||||
hydraURL string
|
hydraURL string
|
||||||
fakeTLSTermination bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogoutReqDoer creates a LogoutRequest.
|
// NewLogoutReqDoer creates a LogoutRequest.
|
||||||
func NewLogoutReqDoer(hydraURL string, fakeTLSTermination bool) *LogoutReqDoer {
|
func NewLogoutReqDoer(hydraURL string) *LogoutReqDoer {
|
||||||
return &LogoutReqDoer{hydraURL: hydraURL, fakeTLSTermination: fakeTLSTermination}
|
return &LogoutReqDoer{hydraURL: hydraURL}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitiateRequest fetches information on the OAuth2 request.
|
// InitiateRequest fetches information on the OAuth2 request.
|
||||||
func (lrd *LogoutReqDoer) InitiateRequest(challenge string) (*ReqInfo, error) {
|
func (lrd *LogoutReqDoer) InitiateRequest(challenge string) (*ReqInfo, error) {
|
||||||
ri, err := initiateRequest(logout, lrd.hydraURL, lrd.fakeTLSTermination, challenge)
|
ri, err := initiateRequest(logout, lrd.hydraURL, challenge)
|
||||||
return ri, errors.Wrap(err, "failed to initiate logout request")
|
return ri, errors.Wrap(err, "failed to initiate logout request")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcceptLogoutRequest accepts the requested logout process, and returns redirect URI.
|
// AcceptLogoutRequest accepts the requested logout process, and returns redirect URI.
|
||||||
func (lrd *LogoutReqDoer) AcceptLogoutRequest(challenge string) (string, error) {
|
func (lrd *LogoutReqDoer) AcceptLogoutRequest(challenge string) (string, error) {
|
||||||
redirectURI, err := acceptRequest(logout, lrd.hydraURL, lrd.fakeTLSTermination, challenge, nil)
|
redirectURI, err := acceptRequest(logout, lrd.hydraURL, challenge, nil)
|
||||||
return redirectURI, errors.Wrap(err, "failed to accept logout request")
|
return redirectURI, errors.Wrap(err, "failed to accept logout request")
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ func TestInitiateLogoutRequest(t *testing.T) {
|
||||||
h := &testInitiateLogoutHandler{reqInfo: tc.reqInfo, status: tc.status}
|
h := &testInitiateLogoutHandler{reqInfo: tc.reqInfo, status: tc.status}
|
||||||
srv := httptest.NewServer(h)
|
srv := httptest.NewServer(h)
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
ldr := hydra.NewLogoutReqDoer(srv.URL, false)
|
ldr := hydra.NewLogoutReqDoer(srv.URL)
|
||||||
|
|
||||||
reqInfo, err := ldr.InitiateRequest(tc.challenge)
|
reqInfo, err := ldr.InitiateRequest(tc.challenge)
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ func TestAcceptLogoutRequest(t *testing.T) {
|
||||||
h := &testAcceptLogoutHandler{challenge: tc.challenge, status: tc.status, redirect: tc.redirect}
|
h := &testAcceptLogoutHandler{challenge: tc.challenge, status: tc.status, redirect: tc.redirect}
|
||||||
srv := httptest.NewServer(h)
|
srv := httptest.NewServer(h)
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
ldr := hydra.NewLogoutReqDoer(srv.URL, false)
|
ldr := hydra.NewLogoutReqDoer(srv.URL)
|
||||||
|
|
||||||
redirect, err := ldr.AcceptLogoutRequest(tc.challenge)
|
redirect, err := ldr.AcceptLogoutRequest(tc.challenge)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ package identp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -31,7 +30,6 @@ type Config struct {
|
||||||
HydraURL string `envconfig:"hydra_url" required:"true" desc:"an admin URL of ORY Hydra Server"`
|
HydraURL string `envconfig:"hydra_url" required:"true" desc:"an admin URL of ORY Hydra Server"`
|
||||||
SessionTTL time.Duration `envconfig:"session_ttl" default:"24h" desc:"a user session's TTL"`
|
SessionTTL time.Duration `envconfig:"session_ttl" default:"24h" desc:"a user session's TTL"`
|
||||||
ClaimScopes map[string]string `envconfig:"claim_scopes" default:"name:profile,family_name:profile,given_name:profile,email:email,https%3A%2F%2Fgithub.com%2Fi-core%2Fwerther%2Fclaims%2Froles:roles" desc:"a mapping of OpenID Connect claims to scopes (all claims are URL encoded)"`
|
ClaimScopes map[string]string `envconfig:"claim_scopes" default:"name:profile,family_name:profile,given_name:profile,email:email,https%3A%2F%2Fgithub.com%2Fi-core%2Fwerther%2Fclaims%2Froles:roles" desc:"a mapping of OpenID Connect claims to scopes (all claims are URL encoded)"`
|
||||||
FakeTLSTermination bool `envconfig:"fake_tls_termination" default:"false" desc:"Fake tls termination by adding \"X-Forwarded-Proto: https\" to http headers "`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserManager is an interface that is used for authentication and providing user's claims.
|
// UserManager is an interface that is used for authentication and providing user's claims.
|
||||||
|
@ -54,7 +52,7 @@ type oidcClaimsFinder interface {
|
||||||
|
|
||||||
// TemplateRenderer renders a template with data and writes it to a http.ResponseWriter.
|
// TemplateRenderer renders a template with data and writes it to a http.ResponseWriter.
|
||||||
type TemplateRenderer interface {
|
type TemplateRenderer interface {
|
||||||
RenderTemplate(w http.ResponseWriter, r *http.Request, name string, data interface{}) error
|
RenderTemplate(w http.ResponseWriter, name string, data interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginTmplData is a data that is needed for rendering the login page.
|
// LoginTmplData is a data that is needed for rendering the login page.
|
||||||
|
@ -85,10 +83,10 @@ func NewHandler(cnf Config, um UserManager, tr TemplateRenderer) *Handler {
|
||||||
// AddRoutes registers all required routes for Login & Consent Provider.
|
// AddRoutes registers all required routes for Login & Consent Provider.
|
||||||
func (h *Handler) AddRoutes(apply func(m, p string, h http.Handler, mws ...func(http.Handler) http.Handler)) {
|
func (h *Handler) AddRoutes(apply func(m, p string, h http.Handler, mws ...func(http.Handler) http.Handler)) {
|
||||||
sessionTTL := int(h.SessionTTL.Seconds())
|
sessionTTL := int(h.SessionTTL.Seconds())
|
||||||
apply(http.MethodGet, "/login", newLoginStartHandler(hydra.NewLoginReqDoer(h.HydraURL, h.FakeTLSTermination, 0), h.tr))
|
apply(http.MethodGet, "/login", newLoginStartHandler(hydra.NewLoginReqDoer(h.HydraURL, 0), h.tr))
|
||||||
apply(http.MethodPost, "/login", newLoginEndHandler(hydra.NewLoginReqDoer(h.HydraURL, h.FakeTLSTermination, sessionTTL), h.um, h.tr))
|
apply(http.MethodPost, "/login", newLoginEndHandler(hydra.NewLoginReqDoer(h.HydraURL, sessionTTL), h.um, h.tr))
|
||||||
apply(http.MethodGet, "/consent", newConsentHandler(hydra.NewConsentReqDoer(h.HydraURL, h.FakeTLSTermination, sessionTTL), h.um, h.ClaimScopes))
|
apply(http.MethodGet, "/consent", newConsentHandler(hydra.NewConsentReqDoer(h.HydraURL, sessionTTL), h.um, h.ClaimScopes))
|
||||||
apply(http.MethodGet, "/logout", newLogoutHandler(hydra.NewLogoutReqDoer(h.HydraURL, h.FakeTLSTermination)))
|
apply(http.MethodGet, "/logout", newLogoutHandler(hydra.NewLogoutReqDoer(h.HydraURL)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// oa2LoginReqAcceptor is an interface that is used for accepting an OAuth2 login request.
|
// oa2LoginReqAcceptor is an interface that is used for accepting an OAuth2 login request.
|
||||||
|
@ -128,8 +126,7 @@ func newLoginStartHandler(rproc oa2LoginReqProcessor, tmplRenderer TemplateRende
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Infow("Failed to initiate an OAuth2 login request", zap.Error(err), "challenge", challenge)
|
log.Infow("Failed to initiate an OAuth2 login request", zap.Error(err), "challenge", challenge)
|
||||||
errMsg := fmt.Sprintf("%s - %s - %s", http.StatusText(http.StatusInternalServerError), err, errors.Cause(err))
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
http.Error(w, errMsg, http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Infow("A login request is initiated", "challenge", challenge, "username", ri.Subject)
|
log.Infow("A login request is initiated", "challenge", challenge, "username", ri.Subject)
|
||||||
|
@ -150,7 +147,7 @@ func newLoginStartHandler(rproc oa2LoginReqProcessor, tmplRenderer TemplateRende
|
||||||
Challenge: challenge,
|
Challenge: challenge,
|
||||||
LoginURL: strings.TrimPrefix(r.URL.String(), "/"),
|
LoginURL: strings.TrimPrefix(r.URL.String(), "/"),
|
||||||
}
|
}
|
||||||
if err := tmplRenderer.RenderTemplate(w, r, loginTmplName, data); err != nil {
|
if err := tmplRenderer.RenderTemplate(w, loginTmplName, data); err != nil {
|
||||||
log.Infow("Failed to render a login page template", zap.Error(err))
|
log.Infow("Failed to render a login page template", zap.Error(err))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -173,7 +170,7 @@ func newLoginEndHandler(ra oa2LoginReqAcceptor, auther authenticator, tmplRender
|
||||||
data := LoginTmplData{
|
data := LoginTmplData{
|
||||||
CSRFToken: nosurf.Token(r),
|
CSRFToken: nosurf.Token(r),
|
||||||
Challenge: challenge,
|
Challenge: challenge,
|
||||||
LoginURL: strings.TrimPrefix(r.URL.String(), "/"),
|
LoginURL: r.URL.String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
username, password := r.Form.Get("username"), r.Form.Get("password")
|
username, password := r.Form.Get("username"), r.Form.Get("password")
|
||||||
|
@ -183,7 +180,7 @@ func newLoginEndHandler(ra oa2LoginReqAcceptor, auther authenticator, tmplRender
|
||||||
data.IsInternalError = true
|
data.IsInternalError = true
|
||||||
log.Infow("Failed to authenticate a login request via the OAuth2 provider",
|
log.Infow("Failed to authenticate a login request via the OAuth2 provider",
|
||||||
zap.Error(err), "challenge", challenge, "username", username)
|
zap.Error(err), "challenge", challenge, "username", username)
|
||||||
if err = tmplRenderer.RenderTemplate(w, r, loginTmplName, data); err != nil {
|
if err = tmplRenderer.RenderTemplate(w, loginTmplName, data); err != nil {
|
||||||
log.Infow("Failed to render a login page template", zap.Error(err))
|
log.Infow("Failed to render a login page template", zap.Error(err))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
@ -191,7 +188,7 @@ func newLoginEndHandler(ra oa2LoginReqAcceptor, auther authenticator, tmplRender
|
||||||
case !ok:
|
case !ok:
|
||||||
data.IsInvalidCredentials = true
|
data.IsInvalidCredentials = true
|
||||||
log.Debugw("Invalid credentials", zap.Error(err), "challenge", challenge, "username", username)
|
log.Debugw("Invalid credentials", zap.Error(err), "challenge", challenge, "username", username)
|
||||||
if err = tmplRenderer.RenderTemplate(w, r, loginTmplName, data); err != nil {
|
if err = tmplRenderer.RenderTemplate(w, loginTmplName, data); err != nil {
|
||||||
log.Infow("Failed to render a login page template", zap.Error(err))
|
log.Infow("Failed to render a login page template", zap.Error(err))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
@ -204,7 +201,7 @@ func newLoginEndHandler(ra oa2LoginReqAcceptor, auther authenticator, tmplRender
|
||||||
if err != nil {
|
if err != nil {
|
||||||
data.IsInternalError = true
|
data.IsInternalError = true
|
||||||
log.Infow("Failed to accept a login request via the OAuth2 provider", zap.Error(err))
|
log.Infow("Failed to accept a login request via the OAuth2 provider", zap.Error(err))
|
||||||
if err := tmplRenderer.RenderTemplate(w, r, loginTmplName, data); err != nil {
|
if err := tmplRenderer.RenderTemplate(w, loginTmplName, data); err != nil {
|
||||||
log.Infow("Failed to render a login page template", zap.Error(err))
|
log.Infow("Failed to render a login page template", zap.Error(err))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,7 @@ func TestHandleLoginStart(t *testing.T) {
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
tmplRenderer := &testTemplateRenderer{
|
tmplRenderer := &testTemplateRenderer{
|
||||||
renderTmplFunc: func(w http.ResponseWriter, r *http.Request, name string, data interface{}) error {
|
renderTmplFunc: func(w http.ResponseWriter, name string, data interface{}) error {
|
||||||
if name != "login.tmpl" {
|
if name != "login.tmpl" {
|
||||||
t.Fatalf("wrong template name: got %q; want \"login.tmpl\"", name)
|
t.Fatalf("wrong template name: got %q; want \"login.tmpl\"", name)
|
||||||
}
|
}
|
||||||
|
@ -264,7 +264,7 @@ func TestHandleLoginEnd(t *testing.T) {
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
tmplRenderer := &testTemplateRenderer{
|
tmplRenderer := &testTemplateRenderer{
|
||||||
renderTmplFunc: func(w http.ResponseWriter, r *http.Request, name string, data interface{}) error {
|
renderTmplFunc: func(w http.ResponseWriter, name string, data interface{}) error {
|
||||||
if name != "login.tmpl" {
|
if name != "login.tmpl" {
|
||||||
t.Fatalf("wrong template name: got %q; want \"login.tmpl\"", name)
|
t.Fatalf("wrong template name: got %q; want \"login.tmpl\"", name)
|
||||||
}
|
}
|
||||||
|
@ -327,11 +327,11 @@ func TestHandleLoginEnd(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type testTemplateRenderer struct {
|
type testTemplateRenderer struct {
|
||||||
renderTmplFunc func(w http.ResponseWriter, r *http.Request, name string, data interface{}) error
|
renderTmplFunc func(w http.ResponseWriter, name string, data interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tl *testTemplateRenderer) RenderTemplate(w http.ResponseWriter, r *http.Request, name string, data interface{}) error {
|
func (tl *testTemplateRenderer) RenderTemplate(w http.ResponseWriter, name string, data interface{}) error {
|
||||||
return tl.renderTmplFunc(w, r, name, data)
|
return tl.renderTmplFunc(w, name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
type testAuthenticator struct {
|
type testAuthenticator struct {
|
||||||
|
|
|
@ -9,7 +9,6 @@ package ldapclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
@ -18,10 +17,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coocood/freecache"
|
"github.com/coocood/freecache"
|
||||||
"github.com/go-ldap/ldap/v3"
|
|
||||||
"github.com/i-core/rlog"
|
"github.com/i-core/rlog"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
ldap "gopkg.in/ldap.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -52,16 +51,12 @@ type Config struct {
|
||||||
BindDN string `envconfig:"binddn" desc:"a LDAP bind DN"`
|
BindDN string `envconfig:"binddn" desc:"a LDAP bind DN"`
|
||||||
BindPass string `envconfig:"bindpw" json:"-" desc:"a LDAP bind password"`
|
BindPass string `envconfig:"bindpw" json:"-" desc:"a LDAP bind password"`
|
||||||
BaseDN string `envconfig:"basedn" required:"true" desc:"a LDAP base DN for searching users"`
|
BaseDN string `envconfig:"basedn" required:"true" desc:"a LDAP base DN for searching users"`
|
||||||
UserSearchQuery string `envconfig:"user_search_query" desc:"the user search query" default:"(&(|(objectClass=organizationalPerson)(objectClass=inetOrgPerson))(|(uid=%[1]s)(mail=%[1]s)(userPrincipalName=%[1]s)(sAMAccountName=%[1]s)))"`
|
|
||||||
AttrClaims map[string]string `envconfig:"attr_claims" default:"name:name,sn:family_name,givenName:given_name,mail:email" desc:"a mapping of LDAP attributes to OpenID connect claims"`
|
AttrClaims map[string]string `envconfig:"attr_claims" default:"name:name,sn:family_name,givenName:given_name,mail:email" desc:"a mapping of LDAP attributes to OpenID connect claims"`
|
||||||
RoleBaseDN string `envconfig:"role_basedn" required:"true" desc:"a LDAP base DN for searching roles"`
|
RoleBaseDN string `envconfig:"role_basedn" required:"true" desc:"a LDAP base DN for searching roles"`
|
||||||
RoleSearchQuery string `envconfig:"role_search_query" desc:"the role search query" default:"(|(&(|(objectClass=group)(objectClass=groupOfNames))(member=%[1]s))(&(objectClass=groupOfUniqueNames)(uniqueMember=%[1]s)))"`
|
|
||||||
RoleAttr string `envconfig:"role_attr" default:"description" desc:"a LDAP group's attribute that contains a role's name"`
|
RoleAttr string `envconfig:"role_attr" default:"description" desc:"a LDAP group's attribute that contains a role's name"`
|
||||||
RoleClaim string `envconfig:"role_claim" default:"https://github.com/i-core/werther/claims/roles" desc:"a name of an OpenID Connect claim that contains user roles"`
|
RoleClaim string `envconfig:"role_claim" default:"https://github.com/i-core/werther/claims/roles" desc:"a name of an OpenID Connect claim that contains user roles"`
|
||||||
CacheSize int `envconfig:"cache_size" default:"512" desc:"a user info cache's size in KiB"`
|
CacheSize int `envconfig:"cache_size" default:"512" desc:"a user info cache's size in KiB"`
|
||||||
CacheTTL time.Duration `envconfig:"cache_ttl" default:"30m" desc:"a user info cache TTL"`
|
CacheTTL time.Duration `envconfig:"cache_ttl" default:"30m" desc:"a user info cache TTL"`
|
||||||
IsTLS bool `envconfig:"is_tls" default:"false" desc:"should LDAP connection be established via TLS"`
|
|
||||||
ConnectionTimeout time.Duration `envconfig:"connection_timeout" default:"60s" desc:"LDAP server connection timeout"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client is a LDAP client (compatible with Active Directory).
|
// Client is a LDAP client (compatible with Active Directory).
|
||||||
|
@ -75,14 +70,7 @@ type Client struct {
|
||||||
func New(cnf Config) *Client {
|
func New(cnf Config) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
Config: cnf,
|
Config: cnf,
|
||||||
connector: &ldapConnector{
|
connector: &ldapConnector{BaseDN: cnf.BaseDN, RoleBaseDN: cnf.RoleBaseDN},
|
||||||
BaseDN: cnf.BaseDN,
|
|
||||||
UserSearchQuery: cnf.UserSearchQuery,
|
|
||||||
RoleBaseDN: cnf.RoleBaseDN,
|
|
||||||
IsTLS: cnf.IsTLS,
|
|
||||||
RoleSearchQuery: cnf.RoleSearchQuery,
|
|
||||||
ConnectionTimeout: cnf.ConnectionTimeout,
|
|
||||||
},
|
|
||||||
cache: freecache.NewCache(cnf.CacheSize * 1024),
|
cache: freecache.NewCache(cnf.CacheSize * 1024),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,7 +183,7 @@ func (cli *Client) FindOIDCClaims(ctx context.Context, username string) (map[str
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
roles := make([]map[string]interface{}, 0)
|
roles := make(map[string]interface{})
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
roleDN, ok := entry["dn"].(string)
|
roleDN, ok := entry["dn"].(string)
|
||||||
if !ok || roleDN == "" {
|
if !ok || roleDN == "" {
|
||||||
|
@ -213,8 +201,21 @@ func (cli *Client) FindOIDCClaims(ctx context.Context, username string) (map[str
|
||||||
if n < k || !strings.EqualFold(roleDN[n-k:], cli.RoleBaseDN) {
|
if n < k || !strings.EqualFold(roleDN[n-k:], cli.RoleBaseDN) {
|
||||||
panic("You should never see that")
|
panic("You should never see that")
|
||||||
}
|
}
|
||||||
|
// The DN without the role's base DN must contain a CN and OU
|
||||||
|
// where the CN is for uniqueness only, and the OU is an application id.
|
||||||
|
path := strings.Split(roleDN[:n-k-1], ",")
|
||||||
|
if len(path) != 2 {
|
||||||
|
log.Infow("A role's DN without the role's base DN must contain two nodes only",
|
||||||
|
"roleBaseDN", cli.RoleBaseDN, "roleDN", roleDN)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
appID := path[1][len("OU="):]
|
||||||
|
|
||||||
roles = append(roles, entry)
|
var appRoles []interface{}
|
||||||
|
if v := roles[appID]; v != nil {
|
||||||
|
appRoles = v.([]interface{})
|
||||||
|
}
|
||||||
|
roles[appID] = append(appRoles, entry[cli.RoleAttr])
|
||||||
}
|
}
|
||||||
claims[cli.RoleClaim] = roles
|
claims[cli.RoleClaim] = roles
|
||||||
|
|
||||||
|
@ -295,45 +296,23 @@ func (cli *Client) findBasicUserDetails(cn conn, username string, attrs []string
|
||||||
type ldapConnector struct {
|
type ldapConnector struct {
|
||||||
BaseDN string
|
BaseDN string
|
||||||
RoleBaseDN string
|
RoleBaseDN string
|
||||||
IsTLS bool
|
|
||||||
UserSearchQuery string
|
|
||||||
RoleSearchQuery string
|
|
||||||
ConnectionTimeout time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ldapConnector) Connect(ctx context.Context, addr string) (conn, error) {
|
func (c *ldapConnector) Connect(ctx context.Context, addr string) (conn, error) {
|
||||||
d := net.Dialer{Timeout: c.ConnectionTimeout}
|
d := net.Dialer{Timeout: ldap.DefaultTimeout}
|
||||||
tcpcn, err := d.DialContext(ctx, "tcp", addr)
|
tcpcn, err := d.DialContext(ctx, "tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ldapcn := ldap.NewConn(tcpcn, false)
|
||||||
if c.IsTLS {
|
|
||||||
tlscn, err := tls.DialWithDialer(&d, "tcp", addr, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tcpcn = tlscn
|
|
||||||
}
|
|
||||||
|
|
||||||
ldapcn := ldap.NewConn(tcpcn, c.IsTLS)
|
|
||||||
|
|
||||||
ldapcn.Start()
|
ldapcn.Start()
|
||||||
return &ldapConn{
|
return &ldapConn{Conn: ldapcn, BaseDN: c.BaseDN, RoleBaseDN: c.RoleBaseDN}, nil
|
||||||
Conn: ldapcn,
|
|
||||||
BaseDN: c.BaseDN,
|
|
||||||
UserSearchQuery: c.UserSearchQuery,
|
|
||||||
RoleBaseDN: c.RoleBaseDN,
|
|
||||||
RoleSearchQuery: c.RoleSearchQuery,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ldapConn struct {
|
type ldapConn struct {
|
||||||
*ldap.Conn
|
*ldap.Conn
|
||||||
BaseDN string
|
BaseDN string
|
||||||
RoleBaseDN string
|
RoleBaseDN string
|
||||||
UserSearchQuery string
|
|
||||||
RoleSearchQuery string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ldapConn) Bind(bindDN, password string) error {
|
func (c *ldapConn) Bind(bindDN, password string) error {
|
||||||
|
@ -345,12 +324,14 @@ func (c *ldapConn) Bind(bindDN, password string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ldapConn) SearchUser(user string, attrs ...string) ([]map[string]interface{}, error) {
|
func (c *ldapConn) SearchUser(user string, attrs ...string) ([]map[string]interface{}, error) {
|
||||||
query := fmt.Sprintf(c.UserSearchQuery, user)
|
query := fmt.Sprintf(
|
||||||
|
"(&(|(objectClass=organizationalPerson)(objectClass=inetOrgPerson))"+
|
||||||
|
"(|(uid=%[1]s)(mail=%[1]s)(userPrincipalName=%[1]s)(sAMAccountName=%[1]s)))", user)
|
||||||
return c.searchEntries(c.BaseDN, query, attrs)
|
return c.searchEntries(c.BaseDN, query, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ldapConn) SearchUserRoles(user string, attrs ...string) ([]map[string]interface{}, error) {
|
func (c *ldapConn) SearchUserRoles(user string, attrs ...string) ([]map[string]interface{}, error) {
|
||||||
query := fmt.Sprintf(c.RoleSearchQuery, user)
|
query := fmt.Sprintf("(&(objectClass=group)(member=%s))", user)
|
||||||
return c.searchEntries(c.RoleBaseDN, query, attrs)
|
return c.searchEntries(c.RoleBaseDN, query, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,26 +1,30 @@
|
||||||
<!DOCTYPE html>
|
{{ define "title" }}
|
||||||
<html lang="{{ (index .LangPrefs 0).Lang }}">
|
Login Provider Werther
|
||||||
<head>
|
{{ end }}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Login Provider Werther</title>
|
{{ define "style" }}
|
||||||
<base href="{{ .WebBasePath }}">
|
|
||||||
<link rel="stylesheet" href="static/style.css">
|
<link rel="stylesheet" href="static/style.css">
|
||||||
</head>
|
{{ end }}
|
||||||
<body>
|
|
||||||
|
{{ define "js" }}
|
||||||
|
<script type="text/javascript" src="static/script.js"></script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<p class="message">
|
<p class="message">
|
||||||
{{ if .Data.IsInvalidCredentials }}
|
{{ if .IsInvalidCredentials }}
|
||||||
Invalid username or password
|
Invalid username or password
|
||||||
{{ else if .Data.IsInternalError }}
|
{{ else if .IsInternalError }}
|
||||||
Internal server error
|
Internal server error
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</p>
|
</p>
|
||||||
<form class="login-form" action="{{ .Data.LoginURL }}" method="POST">
|
<form class="login-form" action="{{ .LoginURL }}" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ .Data.CSRFToken }}">
|
<input type="hidden" name="csrf_token" value={{ .CSRFToken }}>
|
||||||
<input type="hidden" name="login_challenge" value="{{ .Data.Challenge }}">
|
<input type="hidden" name="login_challenge" value={{ .Challenge }}>
|
||||||
|
|
||||||
<input type="text" placeholder="username" name="username"/>
|
<input type="text" placeholder="username" name="username"/>
|
||||||
<input type="password" placeholder="password" name="password"/>
|
<input type="password" placeholder="password" name="password"/>
|
||||||
|
@ -38,6 +42,4 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" src="static/script.js"></script>
|
{{ end }}
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,40 +1,4 @@
|
||||||
/* cyrillic-ext */
|
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
|
||||||
@font-face {
|
|
||||||
font-family: 'Roboto';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: local('Roboto Light'), local('Roboto-Light'), url('./fonts/Roboto-Light.ttf') format('ttf'),
|
|
||||||
url('./fonts/Roboto-Light.woff') format('woff'), url('./fonts/Roboto-Light.woff2') format('woff2');
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Roboto';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: local('Roboto Light'), local('Roboto-Light'), url('./fonts/Roboto-Light.ttf') format('ttf'),
|
|
||||||
url('./fonts/Roboto-Light.woff') format('woff'), url('./fonts/Roboto-Light.woff2') format('woff2');
|
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Roboto';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: local('Roboto Light'), local('Roboto-Light'), url('./fonts/Roboto-Light.ttf') format('ttf'),
|
|
||||||
url('./fonts/Roboto-Light.woff') format('woff'), url('./fonts/Roboto-Light.woff2') format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Roboto';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: local('Roboto Light'), local('Roboto-Light'), url('./fonts/Roboto-Light.ttf') format('ttf'),
|
|
||||||
url('./fonts/Roboto-Light.woff') format('woff'), url('./fonts/Roboto-Light.woff2') format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
|
|
||||||
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
|
@ -1,10 +1,32 @@
|
||||||
external template
|
external template
|
||||||
WebBasePath: testBasePath;
|
WebBasePath: testBasePath;
|
||||||
|
|
||||||
Langs:
|
Title:
|
||||||
ru-RU;q=1,ru;q=0.9,en-US;q=0.8,en;q=0.7,
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Style:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Js:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Content:
|
||||||
|
|
||||||
Data:
|
|
||||||
CSRFToken: testCSRFToken;
|
CSRFToken: testCSRFToken;
|
||||||
Challenge: testChalenge;
|
Challenge: testChalenge;
|
||||||
LoginURL: testLoginURL;
|
LoginURL: testLoginURL;
|
||||||
|
|
|
@ -1,13 +1,31 @@
|
||||||
{{- define "main" }}external template
|
{{- define "title" }}
|
||||||
WebBasePath: {{ .WebBasePath }};
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
Langs:
|
LoginURL: {{ .LoginURL }};
|
||||||
{{ range .LangPrefs }}{{ .Lang }};q={{ .Weight }},{{ end }}
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
Data:
|
{{- end }}
|
||||||
CSRFToken: {{ .Data.CSRFToken }};
|
|
||||||
Challenge: {{ .Data.Challenge }};
|
{{- define "style" }}
|
||||||
LoginURL: {{ .Data.LoginURL }};
|
CSRFToken: {{ .CSRFToken }};
|
||||||
IsInvalidCredentials: {{ .Data.IsInvalidCredentials }};
|
Challenge: {{ .Challenge }};
|
||||||
IsInternalError: {{ .Data.IsInternalError }};
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "js" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "content" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
{{- end }}
|
{{- end }}
|
|
@ -1,9 +1,6 @@
|
||||||
{{- define "main" }}external template
|
{{- define "main" }}external template
|
||||||
WebBasePath: {{ .WebBasePath }};
|
WebBasePath: {{ .WebBasePath }};
|
||||||
|
|
||||||
Langs:
|
|
||||||
{{ range .LangPrefs }}{{ .Lang }};q={{ .Weight }},{{ end }}
|
|
||||||
|
|
||||||
Title:
|
Title:
|
||||||
{{ block "title" .Data }}{{ end }}
|
{{ block "title" .Data }}{{ end }}
|
||||||
|
|
|
@ -1,10 +1,32 @@
|
||||||
internal template
|
internal template
|
||||||
WebBasePath: testBasePath;
|
WebBasePath: testBasePath;
|
||||||
|
|
||||||
Langs:
|
Title:
|
||||||
ru-RU;q=1,ru;q=0.9,en-US;q=0.8,en;q=0.7,
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Style:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Js:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Content:
|
||||||
|
|
||||||
Data:
|
|
||||||
CSRFToken: testCSRFToken;
|
CSRFToken: testCSRFToken;
|
||||||
Challenge: testChalenge;
|
Challenge: testChalenge;
|
||||||
LoginURL: testLoginURL;
|
LoginURL: testLoginURL;
|
||||||
|
|
|
@ -1,13 +1,31 @@
|
||||||
{{- define "main" }}internal template
|
{{- define "title" }}
|
||||||
WebBasePath: {{ .WebBasePath }};
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
Langs:
|
LoginURL: {{ .LoginURL }};
|
||||||
{{ range .LangPrefs }}{{ .Lang }};q={{ .Weight }},{{ end }}
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
Data:
|
{{- end }}
|
||||||
CSRFToken: {{ .Data.CSRFToken }};
|
|
||||||
Challenge: {{ .Data.Challenge }};
|
{{- define "style" }}
|
||||||
LoginURL: {{ .Data.LoginURL }};
|
CSRFToken: {{ .CSRFToken }};
|
||||||
IsInvalidCredentials: {{ .Data.IsInvalidCredentials }};
|
Challenge: {{ .Challenge }};
|
||||||
IsInternalError: {{ .Data.IsInternalError }};
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "js" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "content" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
{{- end }}
|
{{- end }}
|
|
@ -1,9 +1,6 @@
|
||||||
{{- define "main" }}internal template
|
{{- define "main" }}internal template
|
||||||
WebBasePath: {{ .WebBasePath }};
|
WebBasePath: {{ .WebBasePath }};
|
||||||
|
|
||||||
Langs:
|
|
||||||
{{ range .LangPrefs }}{{ .Lang }};q={{ .Weight }},{{ end }}
|
|
||||||
|
|
||||||
Title:
|
Title:
|
||||||
{{ block "title" .Data }}{{ end }}
|
{{ block "title" .Data }}{{ end }}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
external template
|
|
||||||
WebBasePath: testBasePath;
|
|
||||||
|
|
||||||
Langs:
|
|
||||||
ru-RU;q=1,ru;q=0.9,en-US;q=0.8,en;q=0.7,
|
|
||||||
|
|
||||||
Title:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Style:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Js:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Content:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
|
@ -1,31 +0,0 @@
|
||||||
{{- define "title" }}
|
|
||||||
CSRFToken: {{ .CSRFToken }};
|
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "style" }}
|
|
||||||
CSRFToken: {{ .CSRFToken }};
|
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "js" }}
|
|
||||||
CSRFToken: {{ .CSRFToken }};
|
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "content" }}
|
|
||||||
CSRFToken: {{ .CSRFToken }};
|
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
|
@ -1,37 +0,0 @@
|
||||||
internal template
|
|
||||||
WebBasePath: testBasePath;
|
|
||||||
|
|
||||||
Langs:
|
|
||||||
ru-RU;q=1,ru;q=0.9,en-US;q=0.8,en;q=0.7,
|
|
||||||
|
|
||||||
Title:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Style:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Js:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Content:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
|
@ -1,31 +0,0 @@
|
||||||
{{- define "title" }}
|
|
||||||
CSRFToken: {{ .CSRFToken }};
|
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "style" }}
|
|
||||||
CSRFToken: {{ .CSRFToken }};
|
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "js" }}
|
|
||||||
CSRFToken: {{ .CSRFToken }};
|
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "content" }}
|
|
||||||
CSRFToken: {{ .CSRFToken }};
|
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
|
@ -22,7 +22,6 @@ import (
|
||||||
assetfs "github.com/elazarl/go-bindata-assetfs"
|
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||||
"github.com/i-core/routegroup"
|
"github.com/i-core/routegroup"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/text/language"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The file systems provide templates and their resources that are stored in the application's internal assets.
|
// The file systems provide templates and their resources that are stored in the application's internal assets.
|
||||||
|
@ -75,14 +74,8 @@ func NewHTMLRenderer(cnf Config) (*HTMLRenderer, error) {
|
||||||
return &HTMLRenderer{Config: cnf, mainTmpl: mainTmpl, fs: fs}, nil
|
return &HTMLRenderer{Config: cnf, mainTmpl: mainTmpl, fs: fs}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type langPref struct {
|
|
||||||
Lang string
|
|
||||||
Weight float32
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderTemplate renders a HTML page from a template with the specified name using the specified data.
|
// RenderTemplate renders a HTML page from a template with the specified name using the specified data.
|
||||||
func (r *HTMLRenderer) RenderTemplate(w http.ResponseWriter, req *http.Request, name string, data interface{}) error {
|
func (r *HTMLRenderer) RenderTemplate(w http.ResponseWriter, name string, data interface{}) error {
|
||||||
// Read and parse the requested template.
|
|
||||||
f, err := r.fs.Open(name)
|
f, err := r.fs.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if v, ok := err.(*os.PathError); ok {
|
if v, ok := err.(*os.PathError); ok {
|
||||||
|
@ -96,56 +89,20 @@ func (r *HTMLRenderer) RenderTemplate(w http.ResponseWriter, req *http.Request,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read template %q: %s", name, err)
|
return fmt.Errorf("failed to read template %q: %s", name, err)
|
||||||
}
|
}
|
||||||
root, err := template.New("main").Parse(string(b))
|
t, err := r.mainTmpl.Clone()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to clone the main template for template %q: %s", name, err)
|
||||||
|
}
|
||||||
|
t, err = t.Parse(string(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to parse template %q: %s", name, err)
|
return errors.Wrapf(err, "failed to parse template %q: %s", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The old-style template of a web page showed itself as not flexible.
|
|
||||||
// It was changed with a new template that allows overriding the whole page.
|
|
||||||
// The old-style template left for backward compatibility
|
|
||||||
// and will be deprecated in the future major release.
|
|
||||||
if isOldStyleUserTemplate(root) {
|
|
||||||
var wrapper *template.Template
|
|
||||||
wrapper, err = r.mainTmpl.Clone()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to clone the main template for template %q: %s", name, err)
|
|
||||||
}
|
|
||||||
root, err = root.AddParseTree("main", wrapper.Tree)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to create the main template for template %q: %s", name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare template data.
|
|
||||||
basePath := r.BasePath
|
|
||||||
if basePath == "" {
|
|
||||||
basePath = "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
var langPrefs []langPref
|
|
||||||
if acceptLang := req.Header.Get(http.CanonicalHeaderKey("Accept-Language")); acceptLang != "" {
|
|
||||||
var tags []language.Tag
|
|
||||||
var weights []float32
|
|
||||||
tags, weights, err = language.ParseAcceptLanguage(acceptLang)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to parse the header \"Accept-Language\": %s", err)
|
|
||||||
}
|
|
||||||
for i, tag := range tags {
|
|
||||||
langPrefs = append(langPrefs, langPref{Lang: tag.String(), Weight: weights[i]})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
langPrefs = []langPref{{Lang: "en", Weight: 1}}
|
|
||||||
}
|
|
||||||
|
|
||||||
tmplData := map[string]interface{}{"WebBasePath": basePath, "LangPrefs": langPrefs, "Data": data}
|
|
||||||
|
|
||||||
// Render the template.
|
|
||||||
var (
|
var (
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer
|
||||||
bw = bufio.NewWriter(&buf)
|
bw = bufio.NewWriter(&buf)
|
||||||
)
|
)
|
||||||
if err = root.Execute(bw, tmplData); err != nil {
|
if err = t.Execute(bw, map[string]interface{}{"WebBasePath": r.BasePath, "Data": data}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = bw.Flush(); err != nil {
|
if err = bw.Flush(); err != nil {
|
||||||
|
@ -154,38 +111,16 @@ func (r *HTMLRenderer) RenderTemplate(w http.ResponseWriter, req *http.Request,
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
_, err = buf.WriteTo(w)
|
_, err = buf.WriteTo(w)
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if a template is the old-style template.
|
|
||||||
//
|
|
||||||
// A template is considered as the old-style template
|
|
||||||
// if it contains four blocks for customizing the page title,
|
|
||||||
// styles, markup, and scripts.
|
|
||||||
//
|
|
||||||
// See https://github.com/i-core/werther/issues/11.
|
|
||||||
func isOldStyleUserTemplate(root *template.Template) bool {
|
|
||||||
var tmpls []string
|
|
||||||
for _, tmpl := range root.Templates() {
|
|
||||||
tmpls = append(tmpls, tmpl.Name())
|
|
||||||
}
|
|
||||||
contains := func(arr []string, tgt string) bool {
|
|
||||||
for _, item := range arr {
|
|
||||||
if item == tgt {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return contains(tmpls, "title") && contains(tmpls, "style") && contains(tmpls, "js") && contains(tmpls, "content")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainT = `{{ define "main" }}
|
var mainT = `{{ define "main" }}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ (index .LangPrefs 0).Lang }}">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{{ block "title" .Data }}{{ end }}</title>
|
<title>{{ block "title" .Data }}{{ end }}</title>
|
||||||
<base href="{{ .WebBasePath }}">
|
<base href={{ .WebBasePath }}>
|
||||||
{{ block "style" .Data }}{{ end }}
|
{{ block "style" .Data }}{{ end }}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -60,64 +60,23 @@ func TestHTMLRenderer(t *testing.T) {
|
||||||
"IsInternalError": true,
|
"IsInternalError": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
name: "old style internal template not found",
|
|
||||||
wantErr: fmt.Errorf(`the template "login.tmpl" does not exist`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "old style internal template happy path",
|
|
||||||
basePath: "testBasePath",
|
|
||||||
data: map[string]interface{}{
|
|
||||||
"CSRFToken": "testCSRFToken",
|
|
||||||
"Challenge": "testChalenge",
|
|
||||||
"LoginURL": "testLoginURL",
|
|
||||||
"IsInvalidCredentials": true,
|
|
||||||
"IsInternalError": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "old style external template not found",
|
|
||||||
ext: true,
|
|
||||||
wantErr: fmt.Errorf(`the template "login.tmpl" does not exist`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "old style external template happy path",
|
|
||||||
ext: true,
|
|
||||||
basePath: "testBasePath",
|
|
||||||
data: map[string]interface{}{
|
|
||||||
"CSRFToken": "testCSRFToken",
|
|
||||||
"Challenge": "testChalenge",
|
|
||||||
"LoginURL": "testLoginURL",
|
|
||||||
"IsInvalidCredentials": true,
|
|
||||||
"IsInternalError": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
tstDir := path.Join("testdata", t.Name())
|
tstDir := path.Join("testdata", t.Name())
|
||||||
|
|
||||||
|
// Read the main template.
|
||||||
var originMainT = mainT
|
var originMainT = mainT
|
||||||
defer func() { mainT = originMainT }()
|
defer func() { mainT = originMainT }()
|
||||||
|
f, err := os.Open(path.Join(tstDir, "main.tmpl"))
|
||||||
// Read the main template if it is exist.
|
if err != nil {
|
||||||
fpath := path.Join(tstDir, "main.tmpl")
|
|
||||||
stat, err := os.Stat(fpath)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
t.Fatalf("failed to open main template: %s", err)
|
t.Fatalf("failed to open main template: %s", err)
|
||||||
}
|
}
|
||||||
if stat != nil {
|
fc, err := ioutil.ReadAll(f)
|
||||||
var f *os.File
|
if err != nil {
|
||||||
if f, err = os.Open(fpath); err != nil {
|
|
||||||
t.Fatalf("failed to open main template: %s", err)
|
|
||||||
}
|
|
||||||
var fc []byte
|
|
||||||
if fc, err = ioutil.ReadAll(f); err != nil {
|
|
||||||
t.Fatalf("failed to read main template: %s", err)
|
t.Fatalf("failed to read main template: %s", err)
|
||||||
}
|
}
|
||||||
mainT = string(fc)
|
mainT = string(fc)
|
||||||
}
|
|
||||||
|
|
||||||
// Create the template renderer.
|
// Create the template renderer.
|
||||||
cnf := Config{BasePath: tc.basePath}
|
cnf := Config{BasePath: tc.basePath}
|
||||||
|
@ -134,9 +93,7 @@ func TestHTMLRenderer(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
|
err = r.RenderTemplate(rr, "login.tmpl", tc.data)
|
||||||
req.Header.Set(http.CanonicalHeaderKey("Accept-Language"), "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7")
|
|
||||||
err = r.RenderTemplate(rr, req, "login.tmpl", tc.data)
|
|
||||||
|
|
||||||
if tc.wantErr != nil {
|
if tc.wantErr != nil {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -150,11 +107,11 @@ func TestHTMLRenderer(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("\ngot error\n\t%s\nwant no errors", err)
|
t.Fatalf("\ngot error\n\t%s\nwant no errors", err)
|
||||||
}
|
}
|
||||||
f, err := os.Open(path.Join(tstDir, "golden.file"))
|
f, err = os.Open(path.Join(tstDir, "golden.file"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to open golden file: %s", err)
|
t.Fatalf("failed to open golden file: %s", err)
|
||||||
}
|
}
|
||||||
fc, err := ioutil.ReadAll(f)
|
fc, err = ioutil.ReadAll(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to read golden file: %s", err)
|
t.Fatalf("failed to read golden file: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
name: "hydra-werther"
|
|
||||||
arch: "amd64"
|
|
||||||
platform: "linux"
|
|
||||||
version: "${PACKAGE_VERSION}"
|
|
||||||
section: "default"
|
|
||||||
priority: "extra"
|
|
||||||
maintainer: "Cadoles <contact@cadoles.com>"
|
|
||||||
description: |
|
|
||||||
PostgreSQL automated backup scripts
|
|
||||||
vendor: "Cadoles"
|
|
||||||
homepage: "https://forge.cadoles.com/Cadoles/postgres-backup"
|
|
||||||
license: "AGPL-3.0"
|
|
||||||
version_schema: none
|
|
||||||
contents:
|
|
||||||
- src: bin/werther_linux_amd64
|
|
||||||
dst: /usr/bin/hydra-werther
|
|
||||||
- src: conf/hydra-werther.conf
|
|
||||||
dst: /etc/hydra-werther/hydra-werther.conf
|
|
||||||
- src: misc/packaging/systemd/hydra-werther.service
|
|
||||||
dst: /usr/lib/systemd/system/hydra-werther.service
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Run Hydra Werther login/consent/logout app
|
|
||||||
After=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
EnvironmentFile=/etc/hydra-werther/hydra-werther.conf
|
|
||||||
ExecStart=/usr/bin/hydra-werther
|
|
||||||
Restart=on-failure
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
|
@ -1,21 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
DISTS=${DISTS:-linux/386 linux/amd64 windows/amd64 darwin/amd64}
|
|
||||||
|
|
||||||
for dist in $DISTS
|
|
||||||
do
|
|
||||||
os=`echo $dist | cut -d'/' -f1`
|
|
||||||
arch=`echo $dist | cut -d'/' -f2`
|
|
||||||
|
|
||||||
env GOOS=$os GOARCH=$arch go build -o bin/werther_${os}_${arch} -ldflags "-w -s -X main.version=$(git describe --tags)" ./cmd/werther
|
|
||||||
|
|
||||||
if [[ "$os" = "windows" ]]; then
|
|
||||||
zip -r bin/werther_${os}_${arch}.zip bin/werther_${os}_${arch}
|
|
||||||
else
|
|
||||||
tar cvzf bin/werther_${os}_${arch}.tar.gz bin/werther_${os}_${arch}
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
(cd bin && sha256sum *.{tar.gz,zip} > werther_checksums.txt || exit 0)
|
|
Loading…
Reference in New Issue