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:
parent
23c592adeb
commit
100519321e
@ -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 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 r.Host != server.config.AllowedOauthHost {
|
||||
// not behind the proxy; use old authentication method.
|
||||
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
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user