satellite/admin: Allow all operations through Oauth

Allow all the operations when accessing through Oauth, but requires the
authorization token for the ones that we consider that they are
sensitive.

Before these changes, a group of operations weren't available through
Oauth, and people who has access to the authorization token had to
forward the port of the server to their local in order to do them
without Oauth.

These changes shouldn't reduce the security because people who has
access to the authorization token is the same than they can forward the
port and part of those have Oauth access too.

Allowing to perform all the operations through Oauth will improve the
productivity of production owners because they will be able to do all
the administration requests without having to port forward the server.

Change-Id: I6d678abac9f48b9ba5a3c0679ca6b6650df323bb
This commit is contained in:
Ivan Fraixedes 2023-10-30 15:09:27 +01:00
parent 23c592adeb
commit 100519321e
No known key found for this signature in database
GPG Key ID: FB6101AFB5CB5AD5
2 changed files with 89 additions and 32 deletions

View File

@ -34,8 +34,6 @@ import (
)
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.
@ -133,7 +131,7 @@ func NewServer(
// prod owners only
fullAccessAPI := api.NewRoute().Subrouter()
fullAccessAPI.Use(server.withAuth(nil))
fullAccessAPI.Use(server.withAuth([]string{config.Groups.LimitUpdate}, true))
fullAccessAPI.HandleFunc("/users", server.addUser).Methods("POST")
fullAccessAPI.HandleFunc("/users/{useremail}", server.updateUser).Methods("PUT")
fullAccessAPI.HandleFunc("/users/{useremail}", server.deleteUser).Methods("DELETE")
@ -165,7 +163,7 @@ func NewServer(
// limit update access required
limitUpdateAPI := api.NewRoute().Subrouter()
limitUpdateAPI.Use(server.withAuth([]string{config.Groups.LimitUpdate}))
limitUpdateAPI.Use(server.withAuth([]string{config.Groups.LimitUpdate}, false))
limitUpdateAPI.HandleFunc("/users/{useremail}", server.userInfo).Methods("GET")
limitUpdateAPI.HandleFunc("/users/{useremail}/limits", server.userLimits).Methods("GET")
limitUpdateAPI.HandleFunc("/users/{useremail}/limits", server.updateLimits).Methods("PUT")
@ -240,36 +238,26 @@ func (server *Server) SetAllowedOauthHost(host string) {
// withAuth checks if the requester is authorized to perform an operation. If the request did not come from the oauth proxy, verify the auth token.
// Otherwise, check that the user has the required permissions to conduct the operation. `allowedGroups` is a list of groups that are authorized.
// If it is nil, then the api method is not accessible from the oauth proxy.
func (server *Server) withAuth(allowedGroups []string) func(next http.Handler) http.Handler {
func (server *Server) withAuth(allowedGroups []string, requireAPIKey bool) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if server.config.AuthorizationToken == "" {
sendJSONError(w, AuthorizationNotEnabled, "", http.StatusForbidden)
return
}
if r.Host != server.config.AllowedOauthHost {
// not behind the proxy; use old authentication method.
if server.config.AuthorizationToken == "" {
sendJSONError(w, AuthorizationNotEnabled, "", http.StatusForbidden)
return
}
equality := subtle.ConstantTimeCompare(
[]byte(r.Header.Get("Authorization")),
[]byte(server.config.AuthorizationToken),
)
if equality != 1 {
sendJSONError(w, "Forbidden",
"", http.StatusForbidden)
if !validateAPIKey(server.config.AuthorizationToken, r.Header.Get("Authorization")) {
sendJSONError(w, "Forbidden", "required a valid authorization token", http.StatusForbidden)
return
}
} else {
// request made from oauth proxy. Check user groups against allowedGroups.
if allowedGroups == nil {
// Endpoint is a full access endpoint, and requires token auth.
sendJSONError(w, "Forbidden", UnauthorizedThroughOauth, http.StatusForbidden)
return
}
var allowed bool
userGroupsString := r.Header.Get("X-Forwarded-Groups")
userGroups := strings.Split(userGroupsString, ",")
AUTHENTICATED:
for _, userGroup := range userGroups {
if userGroup == "" {
continue
@ -277,18 +265,25 @@ func (server *Server) withAuth(allowedGroups []string) func(next http.Handler) h
for _, permGroup := range allowedGroups {
if userGroup == permGroup {
allowed = true
break
break AUTHENTICATED
}
}
if allowed {
break
}
}
if !allowed {
sendJSONError(w, "Forbidden", fmt.Sprintf(UnauthorizedNotInGroup, allowedGroups), http.StatusForbidden)
return
}
// The operation requires to provide a valid authorization token.
if requireAPIKey && !validateAPIKey(server.config.AuthorizationToken, r.Header.Get("Authorization")) {
sendJSONError(
w, "Forbidden",
"you are part of one of the authorized groups, but this operation requires a valid authorization token",
http.StatusForbidden,
)
return
}
}
server.log.Info(
@ -304,3 +299,8 @@ func (server *Server) withAuth(allowedGroups []string) func(next http.Handler) h
})
}
}
func validateAPIKey(configured, sent string) bool {
equality := subtle.ConstantTimeCompare([]byte(sent), []byte(configured))
return equality == 1
}

View File

@ -77,7 +77,7 @@ func TestBasic(t *testing.T) {
body, err := io.ReadAll(response.Body)
require.NoError(t, response.Body.Close())
require.NoError(t, err)
require.Equal(t, `{"error":"Forbidden","detail":""}`, string(body))
require.Equal(t, `{"error":"Forbidden","detail":"required a valid authorization token"}`, string(body))
})
t.Run("WrongAccess", func(t *testing.T) {
@ -94,7 +94,7 @@ func TestBasic(t *testing.T) {
body, err := io.ReadAll(response.Body)
require.NoError(t, response.Body.Close())
require.NoError(t, err)
require.Equal(t, `{"error":"Forbidden","detail":""}`, string(body))
require.Equal(t, `{"error":"Forbidden","detail":"required a valid authorization token"}`, string(body))
})
t.Run("WithAccess", func(t *testing.T) {
@ -148,7 +148,6 @@ func TestWithOAuth(t *testing.T) {
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)
@ -159,7 +158,9 @@ func TestWithOAuth(t *testing.T) {
body, err := io.ReadAll(response.Body)
require.NoError(t, response.Body.Close())
require.NoError(t, err)
require.Contains(t, string(body), admin.UnauthorizedThroughOauth)
require.Contains(t, string(body), fmt.Sprintf(admin.UnauthorizedNotInGroup,
[]string{planet.Satellites[0].Config.Admin.Groups.LimitUpdate}),
)
})
//
@ -196,6 +197,62 @@ func TestWithOAuth(t *testing.T) {
require.Equal(t, http.StatusOK, response.StatusCode)
})
// Requests of an operation through Oauth that requires the authorization token.
t.Run("AuthorizedThroughOauthWithToken", 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)
// 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.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),
"you are part of one of the authorized groups, but this operation requires a valid authorization token",
)
// with an invalid authorization token should happen the same than not providing it.
req.Header.Set("Authorization", "invalid-token")
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),
"you are part of one of the authorized groups, but this operation requires a valid authorization token",
)
// adding the authorization token should allow this request.
req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken)
response, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, response.Body.Close())
require.Equal(t, http.StatusOK, response.StatusCode)
})
})
}