satellite/admin: add tests to admin auth

This change tests authorization of the admin api.

Issue: https://github.com/storj/storj/issues/5699

Change-Id: Iecfe4c27a70ab1b48aeb5ed3251b51a3406140e8
This commit is contained in:
Wilfred Asomani 2023-04-12 15:06:36 +00:00
parent 49c5e3de9e
commit 4ee22e0ed8
2 changed files with 129 additions and 6 deletions

View File

@ -30,6 +30,15 @@ import (
"storj.io/storj/satellite/payments/stripe"
)
const (
// UnauthorizedThroughOauth - message for full accesses through Oauth.
UnauthorizedThroughOauth = "This operation is not authorized through oauth."
// UnauthorizedNotInGroup - message for when api user is not part of a required access group.
UnauthorizedNotInGroup = "User must be a member of one of these groups to conduct this operation: %s"
// AuthorizationNotEnabled - message for when authorization is disabled.
AuthorizationNotEnabled = "Authorization not enabled."
)
// Config defines configuration for debug server.
type Config struct {
Address string `help:"admin peer http listening address" releaseDefault:"" devDefault:""`
@ -191,8 +200,7 @@ func withAuth(log *zap.Logger, config Config, allowedGroups []string) func(next
if r.Host != config.AllowedOauthHost {
// not behind the proxy; use old authentication method.
if config.AuthorizationToken == "" {
sendJSONError(w, "Authorization not enabled.",
"", http.StatusForbidden)
sendJSONError(w, AuthorizationNotEnabled, "", http.StatusForbidden)
return
}
@ -208,8 +216,8 @@ func withAuth(log *zap.Logger, config Config, allowedGroups []string) func(next
} else {
// request made from oauth proxy. Check user groups against allowedGroups.
if allowedGroups == nil {
sendJSONError(w, "Forbidden",
"This operation is not authorized through oauth. Please contact a production owner to proceed.", http.StatusForbidden)
// Endpoint is a full access endpoint, and requires token auth.
sendJSONError(w, "Forbidden", UnauthorizedThroughOauth, http.StatusForbidden)
return
}
@ -232,8 +240,7 @@ func withAuth(log *zap.Logger, config Config, allowedGroups []string) func(next
}
if !allowed {
sendJSONError(w, "Forbidden",
fmt.Sprintf("User must be a member of one of these groups to conduct this operation: %s", allowedGroups), http.StatusForbidden)
sendJSONError(w, "Forbidden", fmt.Sprintf(UnauthorizedNotInGroup, allowedGroups), http.StatusForbidden)
return
}
}

View File

@ -4,6 +4,7 @@
package admin_test
import (
"fmt"
"io"
"net/http"
"testing"
@ -14,8 +15,10 @@ import (
"storj.io/common/testcontext"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite"
"storj.io/storj/satellite/admin"
)
// TestBasic tests authorization behaviour without oauth.
func TestBasic(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
@ -47,10 +50,13 @@ func TestBasic(t *testing.T) {
require.NoError(t, err)
})
// Testing authorization behavior without Oauth from here on out.
t.Run("NoAccess", func(t *testing.T) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/api/projects/some-id", nil)
require.NoError(t, err)
// This request is not through the Oauth proxy and has no authorization token, it should fail.
response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
@ -99,3 +105,113 @@ func TestBasic(t *testing.T) {
})
})
}
// TestWithOAuth tests authorization behaviour for requests coming through Oauth.
func TestWithOAuth(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
StorageNodeCount: 0,
UplinkCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
config.Admin.Address = "127.0.0.1:52392"
// Make this admin server the AllowedOauthHost so withAuth thinks it's Oauth.
config.Admin.AllowedOauthHost = "127.0.0.1:52392"
config.Admin.StaticDir = "ui/build"
config.Admin.Groups = admin.Groups{LimitUpdate: "LimitUpdate"}
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
projectID := planet.Uplinks[0].Projects[0].ID
address := sat.Admin.Admin.Listener.Addr()
baseURL := "http://" + address.String()
// Requests that require full access should not be accessible through Oauth.
t.Run("UnauthorizedThroughOauth", func(t *testing.T) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/projects/%s/apikeys", baseURL, projectID.String()), nil)
require.NoError(t, err)
req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken)
response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusForbidden, response.StatusCode)
require.Equal(t, "application/json", response.Header.Get("Content-Type"))
body, err := io.ReadAll(response.Body)
require.NoError(t, response.Body.Close())
require.NoError(t, err)
require.Contains(t, string(body), admin.UnauthorizedThroughOauth)
})
//
t.Run("RequireLimitUpdateAccess", func(t *testing.T) {
targetURL := fmt.Sprintf("%s/api/projects/%s/limit", baseURL, projectID.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
require.NoError(t, err)
// this request does not have the {X-Forwarded-Groups: LimitUpdate} header. It should fail.
response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusForbidden, response.StatusCode)
require.Equal(t, "application/json", response.Header.Get("Content-Type"))
body, err := io.ReadAll(response.Body)
require.NoError(t, response.Body.Close())
require.NoError(t, err)
errDetail := fmt.Sprintf(admin.UnauthorizedNotInGroup, []string{planet.Satellites[0].Config.Admin.Groups.LimitUpdate})
require.Contains(t, string(body), errDetail)
req, err = http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
require.NoError(t, err)
// adding the header should allow this request.
req.Header.Set("X-Forwarded-Groups", "LimitUpdate")
response, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, response.Body.Close())
require.Equal(t, http.StatusOK, response.StatusCode)
})
})
}
// TestWithAuthNoToken tests when AuthToken config is set to an empty string (disabled authorization).
func TestWithAuthNoToken(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
StorageNodeCount: 0,
UplinkCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
config.Admin.Address = "127.0.0.1:0"
config.Admin.StaticDir = "ui/build"
// Disable authorization.
config.Console.AuthToken = ""
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
projectID := planet.Uplinks[0].Projects[0].ID
address := sat.Admin.Admin.Listener.Addr()
baseURL := "http://" + address.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/projects/%s/apikeys", baseURL, projectID.String()), nil)
require.NoError(t, err)
// Authorization disabled, so this should fail.
response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusForbidden, response.StatusCode)
require.Equal(t, "application/json", response.Header.Get("Content-Type"))
body, err := io.ReadAll(response.Body)
require.NoError(t, response.Body.Close())
require.NoError(t, err)
require.Contains(t, string(body), admin.AuthorizationNotEnabled)
})
}