Skip to content

Commit

Permalink
Add mapping of OIDC client user to iRODS user
Browse files Browse the repository at this point in the history
When OIDC is enabled, switch off Sqyrrl's current mode of showing only
public (iRODS group) data, but showing it to all clients.

Instead, when OIDC is enabled, Sqyrrl maps the authenticated OIDC
client user to an iRODS user of the same name and then uses the
standard iRODS permissions model. In this mode a client can see
"public" data only if their mapped iRODS user is a member of the
public iRODS group.

Sqyrrl's HTTP session manager is now passed to its constructor so that
it ois accessible to be externally configured and also to simplify
testing because fake OIDC sessions can be set up to test the HTTP
handlers, without the need for an OIDC server or mocks.
  • Loading branch information
kjsanger committed Nov 18, 2024
1 parent 323634e commit c719b24
Show file tree
Hide file tree
Showing 13 changed files with 812 additions and 217 deletions.
14 changes: 9 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,32 @@ VERSION := $(shell git describe --always --tags --dirty)
ldflags := "-X sqyrrl/server.Version=${VERSION}"
build_args := -a -v -ldflags ${ldflags}

build_path = "build/sqyrrl-${VERSION}"
build_path = build/sqyrrl-${VERSION}

CGO_ENABLED := 1
GOARCH := amd64

CGO_ENABLED := 1

.PHONY: build build-linux build-darwin build-windows check clean coverage install lint test

all: build

build: build-linux build-darwin build-windows

build-linux: GOOS = linux
build-linux:
mkdir -p ${build_path}
GOARCH=${GOARCH} GOOS=linux go build ${build_args} -o ${build_path}/sqyrrl-linux-${GOARCH} ./main.go
go build ${build_args} -o ${build_path}/sqyrrl-${GOOS}-${GOARCH} ./main.go

build-darwin: GOOS = darwin
build-darwin:
mkdir -p ${build_path}
GOARCH=${GOARCH} GOOS=darwin go build ${build_args} -o ${build_path}/sqyrrl-darwin-${GOARCH} ./main.go
go build ${build_args} -o ${build_path}/sqyrrl-${GOOS}-${GOARCH} ./main.go

build-windows: GOOS = windows
build-windows:
mkdir -p ${build_path}
GOARCH=${GOARCH} GOOS=windows go build ${build_args} -o ${build_path}/sqyrrl-windows-${GOARCH}.exe ./main.go
go build ${build_args} -o ${build_path}/sqyrrl-${GOOS}-${GOARCH}.exe ./main.go

install:
go install ${build_args}
Expand Down
14 changes: 13 additions & 1 deletion cmd/sqyrrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ package cmd
import (
"fmt"
"github.com/BurntSushi/toml"
"github.com/alexedwards/scs/v2"
"io"
"net/http"
"os"
"strings"
"time"
Expand Down Expand Up @@ -178,8 +180,18 @@ func startServer(cmd *cobra.Command, args []string) (err error) { // NRV
return err
}

// Server-side storage of session data, keyed on a random session ID exchanged with
// the client
sessManager := scs.New()
sessManager.Cookie.Name = "sqyrrl-session" // Session cookie name
sessManager.Cookie.HttpOnly = true // Don't let JS access the cookie
sessManager.Cookie.Persist = false // Don't allow the session to persist across browser sessions
sessManager.Cookie.SameSite = http.SameSiteLaxMode // Can't use Strict because of the OAuth2 callback
sessManager.Cookie.Secure = true // Require HTTPS because SameSite can't be Strict
sessManager.Lifetime = 10 * time.Minute // Session lifetime

var srv *server.SqyrrlServer
srv, err = server.NewSqyrrlServer(logger, &config)
srv, err = server.NewSqyrrlServer(logger, &config, sessManager)
if err != nil {
return err
}
Expand Down
11 changes: 4 additions & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
services:
irods-server:
container_name: irods-server
image: "ghcr.io/wtsi-npg/ub-16.04-irods-4.2.7:latest"
# image: "ghcr.io/wtsi-npg/ub-22.04-irods-4.3.2:latest"
image: "wsinpg/ub-22.04-irods-4.3.3"
platform: linux/amd64
ports:
- "127.0.0.1:1247:1247"
- "127.0.0.1:20000-20199:20000-20199"
restart: always
restart: on-failure
healthcheck:
test: ["CMD", "nc", "-z", "-v", "127.0.0.1", "1247"]
start_period: 30s
Expand All @@ -25,11 +26,7 @@ services:
# The following environment variables may be set in a .env file (files named .env
# are declared in .gitignore):
#
# If no iRODS auth file is provided:
#
# IRODS_PASSWORD
#
# And if using OIDC:
# If using OIDC:
#
# OIDC_CLIENT_ID
# OIDC_ISSUER_URL
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/BurntSushi/toml v1.4.0
github.com/alexedwards/scs/v2 v2.8.0
github.com/coreos/go-oidc/v3 v3.11.0
github.com/cyverse/go-irodsclient v0.14.4
github.com/cyverse/go-irodsclient v0.15.7-0.20241106203458-0b74740d1c86
github.com/microcosm-cc/bluemonday v1.0.26
github.com/onsi/ginkgo/v2 v2.21.0
github.com/onsi/gomega v1.35.1
Expand All @@ -27,6 +27,7 @@ require (
github.com/gorilla/css v1.0.0 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
Expand All @@ -41,9 +42,8 @@ require (
golang.org/x/text v0.19.0 // indirect
golang.org/x/tools v0.26.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

// replace github.com/cyverse/go-irodsclient => ../go-irodsclient

replace github.com/cyverse/go-irodsclient => github.com/wtsi-npg/go-irodsclient v0.0.0-20240417120912-4a4dec5bcefb
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDh
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyverse/go-irodsclient v0.15.7-0.20241106203458-0b74740d1c86 h1:/7oP8j3G42YhOSF8bSNQ7MbYNWPSYV53mw+mScymoic=
github.com/cyverse/go-irodsclient v0.15.7-0.20241106203458-0b74740d1c86/go.mod h1:NN+PxHfLDUmsqfqSY84JfmqXS4EYiuiNW6ti6oPGCgk=
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/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
Expand All @@ -28,6 +30,8 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
Expand Down Expand Up @@ -66,8 +70,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/wtsi-npg/go-irodsclient v0.0.0-20240417120912-4a4dec5bcefb h1:42Ddz94sqVkcgdZjL2QyC2f0yiORTXaCMZAOqeGOFMU=
github.com/wtsi-npg/go-irodsclient v0.0.0-20240417120912-4a4dec5bcefb/go.mod h1:eBXha3cwfrM0p1ijYVqsrLJQHpRwTfpA4c5dKCQsQFc=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
Expand All @@ -93,5 +95,7 @@ google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
84 changes: 57 additions & 27 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"encoding/base64"
"io/fs"
"net/http"
"net/http/httputil"
"path"
"time"

Expand Down Expand Up @@ -111,6 +112,9 @@ func HandleLogin(server *SqyrrlServer) http.Handler {
logger := server.logger
logger.Trace().Msg("LoginHandler called")

req, _ := httputil.DumpRequest(r, true)
logger.Trace().Str("request", string(req)).Msg("HandleLogin request")

if !server.sqyrrlConfig.EnableOIDC {
logger.Error().Msg("OIDC is not enabled")
writeErrorResponse(logger, w, http.StatusForbidden)
Expand All @@ -121,10 +125,19 @@ func HandleLogin(server *SqyrrlServer) http.Handler {

state, err := cryptoRandString(16) // Minimum 128 bits required
if err != nil {
logger.Err(err).Msg("Failed to generate a random state")
writeErrorResponse(logger, w, http.StatusInternalServerError)
return
}

// https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Session_Management_Cheat_Sheet.md#renew-the-session-id-after-any-privilege-level-change
err = server.sessionManager.RenewToken(r.Context())
if err != nil {
logger.Err(err).Msg("Failed to renew session token")
writeErrorResponse(logger, w, http.StatusInternalServerError)
return
}
server.sessionManager.Put(r.Context(), sessionKeyState, state)
server.sessionManager.Put(r.Context(), SessionKeyState, state)

authURL := server.oauth2Config.AuthCodeURL(state)
logger.Info().
Expand All @@ -145,13 +158,16 @@ func HandleAuthCallback(server *SqyrrlServer) http.Handler {
logger := server.logger
logger.Trace().Msg("AuthCallbackHandler called")

req, _ := httputil.DumpRequest(r, true)
logger.Trace().Str("request", string(req)).Msg("HandleAuthCallback request")

if !server.sqyrrlConfig.EnableOIDC {
logger.Error().Msg("OIDC is not enabled")
writeErrorResponse(logger, w, http.StatusForbidden)
return
}

state := server.sessionManager.GetString(r.Context(), sessionKeyState)
state := server.sessionManager.GetString(r.Context(), SessionKeyState)
if state == "" {
logger.Error().Msg("Failed to get a state cookie")
writeErrorResponse(logger, w, http.StatusBadRequest)
Expand Down Expand Up @@ -289,19 +305,9 @@ func HandleIRODSGet(server *SqyrrlServer) http.Handler {
objPath := path.Clean(path.Join("/", r.URL.Path))
logger.Debug().Str("path", objPath).Msg("Getting iRODS data object")

var userID string
if server.isAuthenticated(r) {
userID = iRODSUserIDFromEmail(logger, server.getSessionUserEmail(r))
} else {
userID = IRODSPublicUser
}

userZone := server.iRODSAccount.ClientZone

var err error
var rodsFs *ifs.FileSystem
if rodsFs, err = ifs.NewFileSystemWithDefault(server.iRODSAccount,
AppName); err != nil {
if rodsFs, err = ifs.NewFileSystemWithDefault(server.iRODSAccount, AppName); err != nil {
logger.Err(err).Msg("Failed to create an iRODS file system")
writeErrorResponse(logger, w, http.StatusInternalServerError)
return
Expand Down Expand Up @@ -332,22 +338,46 @@ func HandleIRODSGet(server *SqyrrlServer) http.Handler {
return
}

userZone := server.iRODSAccount.ClientZone

var isReadable bool
isReadable, err = isReadableByUser(logger, rodsFs, userZone, userID, objPath)
if err != nil {
logger.Err(err).Msg("Failed to check if the object is readable")
writeErrorResponse(logger, w, http.StatusInternalServerError)
return
}

if !isReadable {
logger.Info().
Str("path", objPath).
Str("id", userID).
Str("zone", userZone).
Msg("Requested path is not readable by this user")
writeErrorResponse(logger, w, http.StatusForbidden)
return
if server.isAuthenticated(r) {
userName := iRODSUsernameFromEmail(logger, server.getSessionUserEmail(r))
logger.Debug().Str("user", userName).Msg("User is authenticated")

isReadable, err = IsReadableByUser(logger, rodsFs, userName, userZone, objPath)
if err != nil {
logger.Err(err).Msg("Failed to check if the object is readable")
writeErrorResponse(logger, w, http.StatusInternalServerError)
return
}

if !isReadable {
logger.Info().
Str("path", objPath).
Str("user", userName).
Str("zone", userZone).
Msg("Requested path is not readable by this user")
writeErrorResponse(logger, w, http.StatusForbidden)
return
}
} else {
logger.Debug().Msg("User is not authenticated")
isReadable, err = IsPublicReadable(logger, rodsFs, userZone, objPath)
if err != nil {
logger.Err(err).Msg("Failed to check if the object is public readable")
writeErrorResponse(logger, w, http.StatusInternalServerError)
return
}

if !isReadable {
logger.Info().
Str("path", objPath).
Msg("Requested path is not public readable")
writeErrorResponse(logger, w, http.StatusForbidden)
return
}
}

getFileRange(rodsLogger, w, r, rodsFs, objPath)
Expand Down
Loading

0 comments on commit c719b24

Please sign in to comment.