diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2bed8e1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,83 @@ +language: go + +go: + - 1.12.x + +services: + - docker + +env: + global: + - CGO_ENABLED=0 + - GO111MODULE=on + - GOPROXY=https://proxy.golang.org + +cache: + directories: + - "$GOPATH/pkg/mod" + - "$GOPATH/bin" + +install: "(cd $HOME && go get -v github.com/golangci/golangci-lint/cmd/golangci-lint@v1.16.0)" + +script: + - go test -v -coverprofile=coverage.txt ./... + - golangci-lint -v run + - | + set -e + for dist in linux/386 linux/amd64 windows/amd64 darwin/amd64 + 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=$TRAVIS_TAG" ./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) + - | + set -e + docker build --build-arg GOPROXY --build-arg VERSION=$TRAVIS_TAG -t "icoreru/werther:$TRAVIS_COMMIT" . + if [ -n "$TRAVIS_TAG" ]; then + docker tag "icoreru/werther:$TRAVIS_COMMIT" "icoreru/werther:$TRAVIS_TAG" + docker tag "icoreru/werther:$TRAVIS_COMMIT" "icoreru/werther:latest" + fi + +after_success: + - bash <(curl -s https://codecov.io/bash) + +before_deploy: + - | + if [ -n "$TRAVIS_TAG" ]; then + docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" + fi + +deploy: + - provider: releases + api_key: + 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: + - bin/werther_linux_386.tar.gz + - bin/werther_linux_amd64.tar.gz + - bin/werther_windows_amd64.zip + - bin/werther_darwin_amd64.tar.gz + - bin/werther_checksums.txt + skip_cleanup: true + on: + tags: true + condition: $TRAVIS_OS_NAME = linux + + - provider: script + skip_cleanup: true + script: docker push "icoreru/werther:$TRAVIS_TAG" + on: + tags: true + condition: $TRAVIS_OS_NAME = linux + + - provider: script + skip_cleanup: true + script: docker push "icoreru/werther:latest" + on: + tags: true + condition: $TRAVIS_OS_NAME = linux diff --git a/README.md b/README.md index b6dc822..7e7e517 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Werther [1](#myfootnote1) -[![GoDoc][doc-img]][doc] [![Build Status][build-img]][build] [![codecov][codecov-img]][codecov] +[![GoDoc][doc-img]][doc] [![Build Status][build-img]][build] [![codecov][codecov-img]][codecov] [![Go Report Card][goreport-img]][goreport] Werther is an Identity Provider for [ORY Hydra][hydra] over [LDAP][ldap]. It implements [Login And Consent Flow][hydra-login-consent] and provides basic UI. @@ -30,10 +30,10 @@ ORY Hydra v1.0.0-rc.12 or higher. - [Installing](#installing) -- [Usage](#usage) - [Configuration](#configuration) - [User roles](#user-roles) - [UI customization](#ui-customization) +- [Example](#example) - [Resources](#resources) - [Footnotes](#footnotes) - [Contributing](#contributing) @@ -46,7 +46,7 @@ ORY Hydra v1.0.0-rc.12 or higher. ### From Docker ```bash -docker pull icoreru/werter +docker pull icoreru/werther ``` ### From sources @@ -55,44 +55,6 @@ docker pull icoreru/werter go install ./... ``` -## Usage - -1. Create a network: - ``` - docker network create hydra-net - ``` - -2. Run ORY Hydra: - ``` - docker run --network hydra-net -d --restart always --name hydra \ - -p 4444:4444 \ - -p 4445:4445 \ - -e URLS_SELF_ISSUER=http://localhost:4444 \ - -e URLS_SELF_PUBLIC=http://localhost:4444 \ - -e URLS_LOGIN=http://localhost:8080/auth/login \ - -e URLS_CONSENT=http://localhost:8080/auth/consent \ - -e URLS_LOGOUT=http://localhost:8080/auth/logout \ - -e WEBFINGER_OIDC_DISCOVERY_SUPPORTED_SCOPES=profile,email,phone \ - -e WEBFINGER_OIDC_DISCOVERY_SUPPORTED_CLAIMS=name,family_name,given_name,nickname,email,phone_number \ - -e DSN=memory \ - oryd/hydra:v1.0.0-rc.12 serve all - ``` - - Look for details in [ORY Hydra Configuration][hydra-doc-config] and [ORY Hydra Documentation][hydra-doc]. - -3. Run Werther: - ``` - docker run --network hydra-net -d --restart always --name werther \ - -p 8080:8080 \ - -e WERTHER_IDENTP_HYDRA_URL=http://hydra:4445 \ - -e WERTHER_LDAP_ENDPOINTS=icdc0.example.local:389,icdc1.example.local:389 \ - -e WERTHER_LDAP_BINDDN= \ - -e WERTHER_LDAP_BINDPW= \ - -e WERTHER_LDAP_BASEDN="DC=example,DC=local" \ - -e WERTHER_LDAP_ROLE_BASEDN="OU=AppRoles,OU=Domain Groups,DC=example,DC=local" \ - icoreru/werther - ``` - ## Configuration The application is configured via environment variables. @@ -113,16 +75,16 @@ For example, create an OU that repserents an application, and then in the create create groups that represent application's roles: ``` -DC=local -|-- OU=Domain Groups - |-- OU=AppRoles - |-- OU=App1 - |-- CN=app1_role1 (objectClass="group", description="role1") - |-- CN=app1_role2 (objectClass="group", description="role2") +dc=com +|-- dc=example + |-- ou=AppRoles + |-- ou=App1 + |-- cn=app1_role1 (objectClass="group", description="role1") + |-- cn=app1_role2 (objectClass="group", description="role2") ``` Run Werther with the environment variable `WERTHER_LDAP_ROLE_DN` -that equals to `OU=AppRoles,OU=Domain Groups,DC=local`. +that equals to `ou=AppRoles,dc=example,dc=com`. In the above example Werther returns user's roles as a value of the user role's claim `https://github.com/i-core/werther/claims/roles`. @@ -142,23 +104,23 @@ For more details about claims naming see [OpenID Connect Core 1.0][oidc-spec-add For example, when we want to configure multiple applications or several environments for the same application. ``` -DC=local -|-- OU=Domain Groups - |-- OU=AppRoles - |-- OU=Test - |-- OU=App1 - |-- CN=test_app1_role1 (objectClass="group", description="role1") - |-- CN=test_app1_role2 (objectClass="group", description="role2") - |-- OU=App2 - |-- CN=test_app2_role1 (objectClass="group",description-"role1") - |-- CN=test_app2_role2 (objectClass="group",description-"role2") - |-- OU=Dev - |-- OU=App1 - |-- CN=dev_app1_role1 (objectClass="group", description="role1") - |-- CN=dev_app1_role3 (objectClass="group", description="role3") - |-- OU=App2 - |-- CN=dev_app2_role1 (objectClass="group",description-"role1") - |-- CN=dev_app2_role4 (objectClass="group",description-"role4") +dc=com +|-- dc=example + |-- ou=AppRoles + |-- ou=Test + |-- ou=App1 + |-- cn=test_app1_role1 (objectClass="group", description="role1") + |-- cn=test_app1_role2 (objectClass="group", description="role2") + |-- ou=App2 + |-- cn=test_app2_role1 (objectClass="group",description-"role1") + |-- cn=test_app2_role2 (objectClass="group",description-"role2") + |-- ou=Dev + |-- ou=App1 + |-- cn=dev_app1_role1 (objectClass="group", description="role1") + |-- cn=dev_app1_role3 (objectClass="group", description="role3") + |-- ou=App2 + |-- cn=dev_app2_role1 (objectClass="group",description-"role1") + |-- cn=dev_app2_role4 (objectClass="group",description-"role4") ``` Active Directory requires unique CNs in a domain. But in Active Directory @@ -168,7 +130,7 @@ A name of a LDAP attribute is specified using the environment variable `WERTHER_ and has the default value `description`. In the above example, Werther returns a response that contains the next roles: -* when the environment variable `WERTHER_LDAP_ROLE_DN` equals to `OU=Test,OU=AppRoles,OU=Domain Groups,DC=local`: +* when the environment variable `WERTHER_LDAP_ROLE_DN` equals to `ou=Test,ou=AppRoles,dc=example,dc=com`: ```json { "https://github.com/i-core/werther/claims/roles": { @@ -177,7 +139,7 @@ In the above example, Werther returns a response that contains the next roles: } } ``` -* when the environment variable `WERTHER_LDAP_ROLE_DN` equals to `OU=Dev,OU=AppRoles,OU=Domain Groups,DC=local`: +* when the environment variable `WERTHER_LDAP_ROLE_DN` equals to `ou=Dev,ou=AppRoles,dc=example,dc=com`: ```json { "https://github.com/i-core/werther/claims/roles": { @@ -191,21 +153,7 @@ In the above example, Werther returns a response that contains the next roles: Werther uses the Go templates to render UI pages. To customize the UI you should create a directory that contains UI pages' templates. -After that you should set the directory path to the environment variable `WERTHER_WEB_DIR`: - -```bash -docker run --network hydra-net -d --restart always --name werther \ - -p 8080:8080 \ - -v /opt/werther/web:/path/to/custom-login-page/dir \ - -e WERTHER_IDENTP_HYDRA_URL=http://hydra:4445 \ - -e WERTHER_LDAP_ENDPOINTS=icdc0.example.local:389,icdc1.example.local:389 \ - -e WERTHER_LDAP_BINDDN= \ - -e WERTHER_LDAP_BINDPW= \ - -e WERTHER_LDAP_BASEDN="DC=example,DC=local" \ - -e WERTHER_LDAP_ROLE_BASEDN="OU=AppRoles,OU=Domain Groups,DC=example,DC=local" \ - -e WERTHER_WEB_DIR=/opt/werther/web - icoreru/werther -``` +After that you should set the directory path to the environment variable `WERTHER_WEB_DIR`. ### Custom login page @@ -222,11 +170,137 @@ 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). +## Example + +1. Create file `ldap.ldif`: + ``` + dn: uid=kolya_gerasyimov,ou=Users,dc=example,dc=com + objectClass: inetOrgPerson + cn: Kolya Gerasyimov + sn: Gerasyimov + uid: kolya_gerasyimov + userPassword: 123 + mail: kolya_gerasyimov@example.com + ou: Users + + dn: ou=AppRoles,dc=example,dc=com + objectClass: organizationalunit + ou: AppRoles + description: AppRoles + + dn: ou=App1,ou=AppRoles,dc=example,dc=com + objectClass: organizationalunit + ou: App1 + description: App1 + + dn: cn=traveler,ou=App1,ou=AppRoles,dc=example,dc=com + objectClass: groupofnames + cn: traveler + description: traveler + member: uid=kolya_gerasyimov,ou=Users,dc=example,dc=com + ``` + +2. Create file `docker-compose.yml`: + ```yaml + version: "3" + services: + hydra-client: + image: oryd/hydra:v1.0.0-rc.12 + environment: + HYDRA_ADMIN_URL: http://hydra:4445 + command: + - clients + - create + - --skip-tls-verify + - --id + - test-client + - --secret + - test-secret + - --response-types + - id_token,token,"id_token token" + - --grant-types + - implicit + - --scope + - openid,profile,email + - --callbacks + - http://localhost:3000 + - --post-logout-callbacks + - http://localhost:3000/post-logout-callback + networks: + - hydra-net + deploy: + restart_policy: + condition: none + depends_on: + - hydra + hydra: + image: oryd/hydra:v1.0.0-rc.12 + environment: + URLS_SELF_ISSUER: http://localhost:4444 + URLS_SELF_PUBLIC: http://localhost:4444 + URLS_LOGIN: http://localhost:8080/auth/login + URLS_CONSENT: http://localhost:8080/auth/consent + URLS_LOGOUT: http://localhost:8080/auth/logout + WEBFINGER_OIDC_DISCOVERY_SUPPORTED_SCOPES: profile,email,phone + WEBFINGER_OIDC_DISCOVERY_SUPPORTED_CLAIMS: name,family_name,given_name,nickname,email,phone_number + DSN: memory + command: serve all --dangerous-force-http + networks: + - hydra-net + ports: + - "4444:4444" + - "4445:4445" + deploy: + restart_policy: + condition: on-failure + depends_on: + - werther + werther: + image: icoreru/werther:v1.0.0 + environment: + WERTHER_IDENTP_HYDRA_URL: http://hydra:4445 + WERTHER_LDAP_ENDPOINTS: ldap:389 + WERTHER_LDAP_BINDDN: cn=admin,dc=example,dc=com + WERTHER_LDAP_BINDPW: password + WERTHER_LDAP_BASEDN: "dc=example,dc=com" + WERTHER_LDAP_ROLE_BASEDN: "ou=AppRoles,dc=example,dc=com" + networks: + - hydra-net + ports: + - "8080:8080" + deploy: + restart_policy: + condition: on-failure + depends_on: + - ldap + ldap: + image: pgarrett/ldap-alpine + volumes: + - "./ldap.ldif:/ldif/ldap.ldif" + networks: + - hydra-net + ports: + - "389:389" + deploy: + restart_policy: + condition: on-failure + networks: + hydra-net: + ``` + +3. Run the command: + ```bash + docker stack deploy 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&state=12345678. + ## Resources - [Introduction to ORY Hydra, OAuth 2.0, and OpenID Connect][hydra-doc]; - [ORY Hydra: Integrating with (existing) User Management][hydra-login-consent]; -- [Official User Login & Consent Example](https://github.com/ory/hydra-login-consent-node); +- [ORY Hydra: Configuration][hydra-doc-config]; +- [ORY Hydra: Official User Login & Consent Example][hydra-login-consent-example]; - [OpenID Connect Core 1.0][oidc-spec-core]; - [OpenID Connect Session Management 1.0][oidc-spec-session]; - [OpenID Connect Front-Channel Logout 1.0][oidc-spec-front-channel-logout]; @@ -254,6 +328,9 @@ The code in this project is licensed under [MIT license][license]. [codecov-img]: https://codecov.io/gh/i-core/werther/branch/master/graph/badge.svg [codecov]: https://codecov.io/gh/i-core/werther +[goreport-img]: https://goreportcard.com/badge/github.com/i-core/werther +[goreport]: https://goreportcard.com/report/github.com/i-core/werther + [contrib]: https://github.com/i-core/.github/blob/master/CONTRIBUTING.md [license]: LICENSE @@ -263,6 +340,7 @@ The code in this project is licensed under [MIT license][license]. [hydra]: https://www.ory.sh/ [hydra-doc]: https://www.ory.sh/docs/hydra/ [hydra-login-consent]: https://www.ory.sh/docs/hydra/oauth2 +[hydra-login-consent-example]: https://github.com/ory/hydra-login-consent-node [hydra-doc-config]: https://www.ory.sh/docs/hydra/configuration [oidc-spec-core]: https://openid.net/specs/openid-connect-core-1_0.html diff --git a/internal/ldapclient/ldapclient_test.go b/internal/ldapclient/ldapclient_test.go index 37d3264..668320d 100644 --- a/internal/ldapclient/ldapclient_test.go +++ b/internal/ldapclient/ldapclient_test.go @@ -30,8 +30,8 @@ var ( "b": "valB", "c": "valC", "roles": []map[string]interface{}{ - {"dn": "CN=role1,OU=app1,OU=test,DC=local", "test-roles-attr": "r1"}, - {"dn": "CN=role2,OU=app1,OU=test,DC=local", "test-roles-attr": "r2"}, + {"dn": "cn=role1,ou=app1,ou=test,dc=local", "test-roles-attr": "r1"}, + {"dn": "cn=role2,ou=app1,ou=test,dc=local", "test-roles-attr": "r2"}, }, }, { @@ -41,10 +41,10 @@ var ( "b": "valB", "c": "valC", "roles": []map[string]interface{}{ - {"dn": "CN=role1,OU=app1,OU=test,DC=local", "test-roles-attr": "r1"}, - {"dn": "CN=role2,OU=app1,OU=test,DC=local", "test-roles-attr": "r2"}, - {"dn": "CN=role3,OU=app2,OU=test,DC=local", "test-roles-attr": "r3"}, - {"dn": "CN=role4,OU=app2,OU=test,DC=local", "test-roles-attr": "r4"}, + {"dn": "cn=role1,ou=app1,ou=test,dc=local", "test-roles-attr": "r1"}, + {"dn": "cn=role2,ou=app1,ou=test,dc=local", "test-roles-attr": "r2"}, + {"dn": "cn=role3,ou=app2,ou=test,dc=local", "test-roles-attr": "r3"}, + {"dn": "cn=role4,ou=app2,ou=test,dc=local", "test-roles-attr": "r4"}, }, }, { @@ -54,7 +54,7 @@ var ( "b": "valB", "c": "valC", "roles": []map[string]interface{}{ - {"dn": "CN=role1,OU=app1,OU=test,DC=local", "test-roles-attr": "r1"}, + {"dn": "cn=role1,ou=app1,ou=test,dc=local", "test-roles-attr": "r1"}, {"test-roles-attr": "r2"}, }, }, @@ -65,8 +65,8 @@ var ( "b": "valB", "c": "valC", "roles": []map[string]interface{}{ - {"dn": "CN=role1,OU=app1,OU=test,DC=local", "test-roles-attr": "r1"}, - {"dn": "CN=role2,OU=app1,OU=test,DC=local"}, + {"dn": "cn=role1,ou=app1,ou=test,dc=local", "test-roles-attr": "r1"}, + {"dn": "cn=role2,ou=app1,ou=test,dc=local"}, }, }, { @@ -76,7 +76,7 @@ var ( "b": "valB", "c": "valC", "roles": []map[string]interface{}{ - {"dn": "CN=role1,OU=test,DC=local", "test-roles-attr": "r1"}, + {"dn": "cn=role1,ou=test,dc=local", "test-roles-attr": "r1"}, }, }, { @@ -375,7 +375,7 @@ func TestFindOIDCClaims(t *testing.T) { BindDN: tc.bindDN, BindPass: tc.bindPass, AttrClaims: tc.attrClaims, - RoleBaseDN: "OU=test,DC=local", + RoleBaseDN: "ou=test,dc=local", RoleClaim: "test-roles-claim", RoleAttr: "test-roles-attr", }) @@ -409,7 +409,7 @@ func TestClaimsCache(t *testing.T) { client := New(Config{ Endpoints: connector.Endpoints(), AttrClaims: map[string]string{"dn": "name", "a": "claimA", "d": "claimD"}, - RoleBaseDN: "OU=test,DC=local", + RoleBaseDN: "ou=test,dc=local", RoleClaim: "test-roles-claim", RoleAttr: "test-roles-attr", })