-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(keycloak): simplify redeploy logic (#239)
- Loading branch information
Showing
11 changed files
with
298 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package auth | ||
|
||
type ExternalAuthService interface { | ||
Init(url, realm, clientId string, privilegedGroups []string) | ||
Validate(token string) (bool, error) | ||
allowedToRollback(groups []string) bool | ||
} | ||
|
||
func NewExternalAuthService() ExternalAuthService { | ||
return &KeycloakAuthService{} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package auth | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
// A super simple test to check if NewExternalAuthService returns a KeycloakAuthService | ||
// We have it just not to lose test coverage percentage at the moment | ||
func TestNewExternalAuthService(t *testing.T) { | ||
service := NewExternalAuthService() | ||
assert.IsType(t, &KeycloakAuthService{}, service) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package auth | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/shini4i/argo-watcher/internal/helpers" | ||
|
||
"github.com/rs/zerolog/log" | ||
) | ||
|
||
type KeycloakResponse struct { | ||
Groups []string `json:"groups"` | ||
} | ||
|
||
type KeycloakAuthService struct { | ||
Url string | ||
Realm string | ||
ClientId string | ||
PrivilegedGroups []string | ||
client *http.Client | ||
} | ||
|
||
// Init is used to initialize KeycloakAuthService with Keycloak URL, realm and client ID | ||
func (k *KeycloakAuthService) Init(url, realm, clientId string, privilegedGroups []string) { | ||
k.Url = url | ||
k.Realm = realm | ||
k.ClientId = clientId | ||
k.PrivilegedGroups = privilegedGroups | ||
k.client = &http.Client{} | ||
} | ||
|
||
// Validate implements quite simple token validation approach | ||
// We just call Keycloak userinfo endpoint and check if it returns 200 | ||
// effectively delegating token validation to Keycloak | ||
func (k *KeycloakAuthService) Validate(token string) (bool, error) { | ||
var keycloakResponse KeycloakResponse | ||
|
||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/realms/%s/protocol/openid-connect/userinfo", k.Url, k.Realm), nil) | ||
if err != nil { | ||
return false, fmt.Errorf("error creating request: %v", err) | ||
} | ||
req.Header.Add("Authorization", "Bearer "+token) | ||
|
||
resp, err := k.client.Do(req) | ||
if err != nil { | ||
return false, fmt.Errorf("error on response: %v", err) | ||
} | ||
defer func(Body io.ReadCloser) { | ||
err := Body.Close() | ||
if err != nil { | ||
log.Error().Msgf("error closing response body: %v", err) | ||
} | ||
}(resp.Body) | ||
|
||
// Read and print the response body | ||
bodyBytes, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
log.Error().Msgf("error reading response body: %v", err) | ||
} else { | ||
if err := json.Unmarshal(bodyBytes, &keycloakResponse); err != nil { | ||
log.Error().Msgf("error unmarshalling response body: %v", err) | ||
} | ||
} | ||
|
||
userPrivileged := k.allowedToRollback(keycloakResponse.Groups) | ||
|
||
if resp.StatusCode == http.StatusOK && userPrivileged { | ||
return true, nil | ||
} else if resp.StatusCode == http.StatusOK && !userPrivileged { | ||
return false, fmt.Errorf("user is not a member of any of the privileged groups") | ||
} | ||
|
||
return false, fmt.Errorf("token validation failed with status: %v", resp.Status) | ||
} | ||
|
||
// allowedToRollback checks if the user is a member of any of the privileged groups | ||
// It duplicates the logic from fronted just to be sure that the user did not generate the request manually | ||
func (k *KeycloakAuthService) allowedToRollback(groups []string) bool { | ||
for _, group := range groups { | ||
if helpers.Contains(k.PrivilegedGroups, group) { | ||
log.Debug().Msgf("User is a member of the privileged group: %v", group) | ||
return true | ||
} | ||
} | ||
|
||
log.Debug().Msgf("User is not a member of any of the privileged groups: %v", k.PrivilegedGroups) | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package auth | ||
|
||
import ( | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestKeycloakAuthService_Init(t *testing.T) { | ||
service := &KeycloakAuthService{} | ||
|
||
url := "http://localhost:8080/auth" | ||
realm := "test" | ||
clientId := "test" | ||
|
||
service.Init(url, realm, clientId, []string{}) | ||
|
||
assert.Equal(t, url, service.Url) | ||
assert.Equal(t, realm, service.Realm) | ||
assert.Equal(t, clientId, service.ClientId) | ||
assert.IsType(t, &http.Client{}, service.client) | ||
} | ||
|
||
func TestKeycloakAuthService_Validate(t *testing.T) { | ||
t.Run("should return true if token is valid and user is in privileged group", func(t *testing.T) { | ||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { | ||
assert.Equal(t, req.URL.String(), "/realms/test/protocol/openid-connect/userinfo") | ||
rw.WriteHeader(http.StatusOK) | ||
_, err := rw.Write([]byte(`{"groups": ["group1"]}`)) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
})) | ||
defer server.Close() | ||
|
||
service := &KeycloakAuthService{ | ||
Url: server.URL, | ||
Realm: "test", | ||
ClientId: "test", | ||
PrivilegedGroups: []string{"group1"}, | ||
client: server.Client(), | ||
} | ||
|
||
ok, err := service.Validate("test") | ||
|
||
assert.NoError(t, err) | ||
assert.True(t, ok) | ||
}) | ||
|
||
t.Run("should return false if token is valid but user is not in privileged group", func(t *testing.T) { | ||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { | ||
assert.Equal(t, req.URL.String(), "/realms/test/protocol/openid-connect/userinfo") | ||
rw.WriteHeader(http.StatusOK) | ||
_, err := rw.Write([]byte(`{"groups": ["group2"]}`)) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
})) | ||
defer server.Close() | ||
|
||
service := &KeycloakAuthService{ | ||
Url: server.URL, | ||
Realm: "test", | ||
ClientId: "test", | ||
PrivilegedGroups: []string{"group1"}, | ||
client: server.Client(), | ||
} | ||
|
||
ok, err := service.Validate("test") | ||
|
||
assert.Error(t, err) | ||
assert.False(t, ok) | ||
}) | ||
|
||
t.Run("should return false if token is invalid", func(t *testing.T) { | ||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { | ||
assert.Equal(t, req.URL.String(), "/realms/test/protocol/openid-connect/userinfo") | ||
rw.WriteHeader(http.StatusUnauthorized) | ||
_, err := rw.Write([]byte(`Unauthorized`)) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
})) | ||
defer server.Close() | ||
|
||
service := &KeycloakAuthService{ | ||
Url: server.URL, | ||
Realm: "test", | ||
ClientId: "test", | ||
client: server.Client(), | ||
} | ||
|
||
ok, err := service.Validate("test") | ||
|
||
assert.Error(t, err) | ||
assert.False(t, ok) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.