Skip to content

Commit

Permalink
Add an Authoriser interface and use it to add mocks for testing
Browse files Browse the repository at this point in the history
The Authoriser interface implements a small subset of the
go-irodsclient API sufficient to create mocks to emulate having users
and groups from federated zones.

go-irodsclient doesn't have full federation support, but nevertheless
federated users are detected and reported by its API. The aim is to
allow some level of testing against them without requiring a real
federated iRODS servers.

This commit also includes some refactoring to reduce the number of
parameters used by some functions, particularly where user/group names
and zones are being passed seperately, but are actually related.
  • Loading branch information
kjsanger committed Feb 13, 2025
1 parent 26b0e34 commit 3beb2f1
Show file tree
Hide file tree
Showing 6 changed files with 560 additions and 261 deletions.
101 changes: 48 additions & 53 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ func HandleHomePage(server *SqyrrlServer) http.Handler {
}

// RedirectToIdentityServer redirects the user to the identity server for use within
// the LoginHandler and iRODSGetHandler on finding authenticaiton required.
func RedirectToIdentityServer(w http.ResponseWriter, r *http.Request, server *SqyrrlServer, redirect_uri string) {
// the LoginHandler and iRODSGetHandler on finding that authentication is required.
func RedirectToIdentityServer(w http.ResponseWriter, r *http.Request, server *SqyrrlServer, redirectUri string) {
logger := server.logger
logger.Trace().Msg("LoginHandler called")

Expand Down Expand Up @@ -142,13 +142,14 @@ func RedirectToIdentityServer(w http.ResponseWriter, r *http.Request, server *Sq
}
server.sessionManager.Put(r.Context(), SessionKeyState, state)
// store where to send the user after login
server.sessionManager.Put(r.Context(), RedirectURIState, redirect_uri)
server.sessionManager.Put(r.Context(), RedirectURIState, redirectUri)

authURL := server.oauth2Config.AuthCodeURL(state)
logger.Info().
Str("auth_url", authURL).
Str("auth_redirect_url", authURL).
Str("state", state).
Str("eventual_redirect_uri", redirect_uri).
Str("eventual_redirect_uri", redirectUri).
Msg("Redirecting to auth URL")

http.Redirect(w, r, authURL, http.StatusFound)
Expand Down Expand Up @@ -243,12 +244,15 @@ func HandleAuthCallback(server *SqyrrlServer) http.Handler {
Str("email", claims.Email).
Msg("User logged in")

logger.Debug().Msg("Redirecting logged in user to home page")
// find where to send the user after login - could be the home page or a path requiring auth
redirect_uri := "/" + server.sessionManager.GetString(r.Context(), RedirectURIState)
redirectUri := "/" + server.sessionManager.GetString(r.Context(), RedirectURIState)

logger.Debug().Str("redirect_uri", redirect_uri).Msg("Redirecting logged in user")
logger.Debug().
Str("redirect_uri", redirectUri).
Msg("Redirecting logged in user")

http.Redirect(w, r, redirect_uri, http.StatusFound)
http.Redirect(w, r, redirectUri, http.StatusFound)
})
}

Expand Down Expand Up @@ -305,19 +309,12 @@ func HandleIRODSGet(server *SqyrrlServer) http.Handler {
logger := server.logger
logger.Trace().Msg("iRODS get handler called")

var corrID string
if val := r.Context().Value(correlationIDKey); val != nil {
corrID = val.(string)
}

rodsLogger := logger.With().
Str("correlation_id", corrID).
Str("irods", "get").Logger()

// The path should be clean as it has passed through the ServeMux, but since we're
// doing a path.Join, clean it before passing it to iRODS
objPath := path.Clean(path.Join("/", r.URL.Path))
logger.Debug().Str("path", objPath).Msg("Getting iRODS data object")

pathLogger := logger.With().Str("path", objPath).Logger()
pathLogger.Debug().Msg("Getting iRODS data object")

var err error
var rodsFs *ifs.FileSystem
Expand All @@ -334,20 +331,16 @@ func HandleIRODSGet(server *SqyrrlServer) http.Handler {
// filesystem.StatFile(objPath) is better because we can check for the error type.
if _, err = rodsFs.Stat(objPath); err != nil {
if types.IsAuthError(err) {
logger.Err(err).
Str("path", objPath).
Msg("Failed to authenticate with iRODS")
pathLogger.Err(err).Msg("Failed to authenticate with iRODS")
writeErrorResponse(logger, w, http.StatusUnauthorized)
return
}
if types.IsFileNotFoundError(err) {
logger.Info().
Str("path", objPath).
Msg("Requested path does not exist")
pathLogger.Info().Msg("Requested path does not exist")
writeErrorResponse(logger, w, http.StatusNotFound)
return
}
logger.Err(err).Str("path", objPath).Msg("Failed to stat file")
pathLogger.Err(err).Msg("Failed to stat file")
writeErrorResponse(logger, w, http.StatusInternalServerError)
return
}
Expand All @@ -362,52 +355,54 @@ func HandleIRODSGet(server *SqyrrlServer) http.Handler {
return
}

if isReadable {
logger.Debug().
Str("path", objPath).
Msg("Requested path is public readable")
} else {
if !isReadable {
if server.isAuthenticated(r) {
// The username obtained from the email address does not include the iRODS
// zone. We use the local zone to which the Sqyrrl server is connected as
// the user's zone.
userName := iRODSUsernameFromEmail(logger, server.getSessionUserEmail(r))
userZone := server.sqyrrlConfig.IRODSZoneForOIDC
name := iRODSUsernameFromEmail(logger, server.getSessionUserEmail(r))
zone := server.sqyrrlConfig.IRODSZoneForOIDC
userName := types.IRODSUser{Name: name, Zone: zone}

logger.Debug().Str("user", userName).Msg("User is authenticated")
userLogger := logger.With().
Str("user", name).
Str("zone", zone).Logger()
userLogger.Debug().Msg("User authenticated")

isReadable, err = IsReadableByUser(logger, rodsFs, localZone, userName, userZone, objPath)
isReadable, err = IsReadableByUser(logger, rodsFs, localZone, userName, objPath)
if err != nil {
logger.Err(err).Msg("Failed to check if the object is readable")
writeErrorResponse(logger, w, http.StatusInternalServerError)
userLogger.Err(err).Msg("Failed to check if the object is readable")
writeErrorResponse(pathLogger, 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)
userLogger.Info().Msg("Requested path is not readable by this user")
writeErrorResponse(pathLogger, w, http.StatusForbidden)
return
}
} else if server.sqyrrlConfig.EnableOIDC {
logger.Debug().Msg("User is not authenticated")

logger.Info().
Str("path", objPath).
Msg("Requested path is not public readable - redirecting to login")
RedirectToIdentityServer(w, r, server, r.URL.Path)
return
} else {
logger.Info().
Str("path", objPath).
Msg("Requested path is not public readable - and no OIDC enabled")
writeErrorResponse(logger, w, http.StatusForbidden)
if server.sqyrrlConfig.EnableOIDC {
pathLogger.Debug().Msg("User is not authenticated")
pathLogger.Info().Msg("Requested path is not public readable - redirecting to login")
RedirectToIdentityServer(w, r, server, r.URL.Path)
} else {
pathLogger.Info().Msg("Requested path is not public readable - and no OIDC enabled")
writeErrorResponse(logger, w, http.StatusForbidden)
}
return
}
} else {
pathLogger.Debug().Msg("Requested path is public readable")
}

var corrID string
if val := r.Context().Value(correlationIDKey); val != nil {
corrID = val.(string)
}
rodsLogger := logger.With().
Str("correlation_id", corrID).
Str("irods", "get").Logger()

getFileRange(rodsLogger, w, r, rodsFs, objPath)
})
Expand Down
17 changes: 11 additions & 6 deletions server/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,11 @@ var _ = Describe("iRODS Get Handler", func() {
var c context.Context
// There is no session for this token, so a new session will always be created
c, err = sessManager.Load(r.Context(), sessionToken)

user := server.ParseUser(userNotInPublic)
sessManager.Put(c, server.SessionKeyAccessToken, accessToken)
sessManager.Put(c, server.SessionKeyUserName, userNotInPublic)
sessManager.Put(c, server.SessionKeyUserEmail, userNotInPublic+"@sanger.ac.uk")
sessManager.Put(c, server.SessionKeyUserName, user.Name)
sessManager.Put(c, server.SessionKeyUserEmail, user.Name+"@sanger.ac.uk")
r = r.WithContext(c)

// A real session token is created here, but we don't need it
Expand All @@ -210,8 +212,9 @@ var _ = Describe("iRODS Get Handler", func() {
conn, err = irodsFS.GetIOConnection()
Expect(err).NotTo(HaveOccurred())

group := server.ParseUser(populatedGroup)
err = ifs.ChangeDataObjectAccess(conn, remotePath, types.IRODSAccessLevelReadObject,
populatedGroup, testZone, false)
group.Name, group.Zone, false)
Expect(err).NotTo(HaveOccurred())
}, NodeTimeout(time.Second*5))

Expand Down Expand Up @@ -249,7 +252,7 @@ var _ = Describe("iRODS Get Handler", func() {
Expect(err).NotTo(HaveOccurred())
})

It("should return Ok", func(ctx SpecContext) {
It("should return OK", func(ctx SpecContext) {
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, r)

Expand All @@ -265,9 +268,11 @@ var _ = Describe("iRODS Get Handler", func() {
var c context.Context
// There is no session for this token, so a new session will always be created
c, err = sessManager.Load(r.Context(), sessionToken)

user := server.ParseUser(userInPublic)
sessManager.Put(c, server.SessionKeyAccessToken, accessToken)
sessManager.Put(c, server.SessionKeyUserName, userInPublic)
sessManager.Put(c, server.SessionKeyUserEmail, userInPublic+"@sanger.ac.uk")
sessManager.Put(c, server.SessionKeyUserName, user.Name)
sessManager.Put(c, server.SessionKeyUserEmail, user.Name+"@sanger.ac.uk")
r = r.WithContext(c)

// A real session token is created here, but we don't need it
Expand Down
Loading

0 comments on commit 3beb2f1

Please sign in to comment.