Skip to content

Commit

Permalink
chore: refactor auth logic (#426)
Browse files Browse the repository at this point in the history
Co-authored-by: decleaver <85503726+decleaver@users.noreply.github.com>
  • Loading branch information
UncleGedd and decleaver authored Oct 15, 2024
1 parent 6c4076a commit caa7e8f
Show file tree
Hide file tree
Showing 36 changed files with 470 additions and 368 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/tech_debt.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: Tech debt
about: Record something that should be investigated or refactored in the future.
title: ''
labels: 'tech-debt'
labels: 'tech debt'
assignees: ''
---

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/api-auth-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
uses: ./.github/actions/setup

- name: Run tests
run: uds run test:api-auth
run: uds run test:local-auth
timeout-minutes: 30

- name: Debug Output
Expand All @@ -42,4 +42,4 @@ jobs:
if: always()
uses: defenseunicorns/uds-common/.github/actions/save-logs@e3008473beab00b12a94f9fcc7340124338d5c08 # v0.13.1
with:
suffix: api-auth
suffix: local-auth
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Most of the actions needed for running and testing UDS Runtime are contained in
To view a complete list of all runnable tasks, run `uds run --list-all`.

API authentication is enabled by default. To disable it, you can set the `API_AUTH_DISABLED` environment variable to true when running the backend. When running the backend and frontend locally with API auth enabled, when you start the backend, it will print a URL to the console with the api token query parameter as well as launch the app in your browser. If you are also running the frontend locally (via `npm run dev`), you will want to grab the token and update the url in your browser to use port `:5173` which is used by default. Example: `http://localhost:5173/auth?token=your-token-here`. More information on API authentication can be found in the [API Auth docs](./docs/api-auth.md).
Local API authentication is enabled by default. To disable it, you can set the `LOCAL_AUTH_ENABLED` environment variable to false when running the backend. When running the backend and frontend locally with API auth enabled, when you start the backend, it will print a URL to the console with the api token query parameter as well as launch the app in your browser. If you are also running the frontend locally (via `npm run dev`), you will want to grab the token and update the url in your browser to use port `:5173` which is used by default. Example: `http://localhost:5173/auth?token=your-token-here`. More information on API authentication can be found in the [API Auth docs](./docs/api-auth.md).

### Pre-Commit Hooks and Linting

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ USER 65532:65532
# copy binary from local and expose port
COPY --chown=65532:65532 build/uds-runtime-linux-${TARGETARCH} /app/uds-runtime
ENV PORT=8080
ENV API_AUTH_DISABLED=true
ENV LOCAL_AUTH_ENABLED=false
EXPOSE 8080

# run binary
Expand Down
2 changes: 1 addition & 1 deletion chart/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ spec:
memory: {{ .Values.resources.limits.memory | quote }}
cpu: {{ .Values.resources.limits.cpu | quote }}
env:
- name: AUTH_SVC_ENABLED
- name: IN_CLUSTER_AUTH_ENABLED
value: {{ .Values.sso.enabled | quote }}
85 changes: 0 additions & 85 deletions design-docs/0001-cluster-disconnection.md

This file was deleted.

33 changes: 0 additions & 33 deletions design-docs/0002-api-auth.md

This file was deleted.

Binary file removed design-docs/images/api-auth-flow.png
Binary file not shown.
48 changes: 0 additions & 48 deletions design-docs/template.md

This file was deleted.

54 changes: 54 additions & 0 deletions pkg/api/auth/cluster/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2024 Defense Unicorns
// SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial

package cluster

import (
"net/http"
"strings"

"github.com/golang-jwt/jwt/v5"
)

var allowedGroups = []string{
"/UDS Core/Admin",
"/UDS Core/Auditor",
}

// ValidateJWT checks if the request has a valid JWT token with the required groups.
func ValidateJWT(w http.ResponseWriter, r *http.Request) bool {
authHeader := r.Header.Get("Authorization")

if authHeader == "" {
http.Error(w, "Missing Authorization header", http.StatusUnauthorized)
return false
}

tokenString := strings.TrimPrefix(authHeader, "Bearer ")

// parse the JWT token without validation (authservice will validate it, we only need the groups here)
token, _, err := jwt.NewParser(jwt.WithoutClaimsValidation()).ParseUnverified(tokenString, jwt.Claims(jwt.MapClaims{}))
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return false
}

// Check if the token contains a "groups" claim
if groups, ok := token.Claims.(jwt.MapClaims)["groups"].([]interface{}); ok {
// Check if any of the token's groups match the allowed groups
for _, group := range groups {
for _, allowedGroup := range allowedGroups {
if group == allowedGroup {
// Group is allowed
return true
}
}
}
// If we reach here, no matching group was found
http.Error(w, "Insufficient permissions", http.StatusForbidden)
return false
}

http.Error(w, "Invalid token claims", http.StatusUnauthorized)
return false
}
57 changes: 13 additions & 44 deletions pkg/api/auth/session_test.go → pkg/api/auth/cluster/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

//go:build unit

package auth
package cluster

import (
"net/http"
Expand All @@ -14,47 +14,7 @@ import (
"github.com/stretchr/testify/require"
)

func TestStoreSession(t *testing.T) {
storage := NewInMemoryStorage()
sessionID := "test-session-id"

storage.StoreSession(sessionID)

require.Equal(t, sessionID, storage.sessionID, "expected sessionID to be stored correctly")
}

func TestValidateSession(t *testing.T) {
storage := NewInMemoryStorage()
sessionID := "test-session-id"

storage.StoreSession(sessionID)

require.True(t, storage.ValidateSession(sessionID), "expected sessionID to be valid")

invalidSessionID := "invalid-session-id"
require.False(t, storage.ValidateSession(invalidSessionID), "expected invalid sessionID to be invalid")
}

func TestRemoveSession(t *testing.T) {
storage := NewInMemoryStorage()
sessionID := "test-session-id"

storage.StoreSession(sessionID)
storage.RemoveSession()

require.Empty(t, storage.sessionID, "expected sessionID to be empty after removal")
require.False(t, storage.ValidateSession(sessionID), "expected sessionID to be invalid after removal")
}

func TestRequireJWT(t *testing.T) {
// Create a sample handler that the middleware will wrap
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

// Create the middleware
middleware := RequireJWT(nextHandler)

func TestValidateJWT(t *testing.T) {
// Helper function to create a JWT token without signing
createToken := func(groups []string) string {
claims := jwt.MapClaims{
Expand All @@ -75,6 +35,11 @@ func TestRequireJWT(t *testing.T) {
token: createToken([]string{"/UDS Core/Admin"}),
expectedStatus: http.StatusOK,
},
{
name: "Valid token with another allowed group",
token: createToken([]string{"/UDS Core/Auditor"}),
expectedStatus: http.StatusOK,
},
{
name: "Valid token without allowed group",
token: createToken([]string{"guest"}),
Expand Down Expand Up @@ -108,11 +73,15 @@ func TestRequireJWT(t *testing.T) {
// Create a ResponseRecorder to record the response
rr := httptest.NewRecorder()

// Call the middleware
middleware.ServeHTTP(rr, req)
// Call the function directly
result := ValidateJWT(rr, req)

// Check the status code
require.Equal(t, tt.expectedStatus, rr.Code, "handler returned wrong status code")

// Check the return value
expectedResult := tt.expectedStatus == http.StatusOK
require.Equal(t, expectedResult, result, "ValidateJWT returned unexpected result")
})
}
}
Loading

0 comments on commit caa7e8f

Please sign in to comment.