73eae98929
schemas / vulnerabilities (pull_request) Successful in 2m15s
schemas / check-release (pull_request) Successful in 2m17s
schemas / check (pull_request) Successful in 4m48s
pre-commit / pre-commit (pull_request) Successful in 5m58s
schemas / build (pull_request) Successful in 3m36s
schemas / deploy-prod (pull_request) Has been skipped
- Update git remote to git.unbound.se - Add Gitea workflows: ci.yaml, pre-commit.yaml, release.yaml, goreleaser.yaml - Delete .gitlab-ci.yml - Update Go module path to gitea.unbound.se/unboundsoftware/schemas - Update all imports to new module path - Update Docker registry to oci.unbound.se - Update .goreleaser.yml for Gitea releases with internal cluster URL - Remove GitLab CI linter from pre-commit config - Use shared release workflow with tag_only for versioning Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
150 lines
3.7 KiB
Go
150 lines
3.7 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/99designs/gqlgen/graphql"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
|
)
|
|
|
|
const (
|
|
UserKey = ContextKey("user")
|
|
OrganizationKey = ContextKey("organization")
|
|
)
|
|
|
|
type Cache interface {
|
|
OrganizationByAPIKey(apiKey string) *domain.Organization
|
|
}
|
|
|
|
func NewAuth(cache Cache) *AuthMiddleware {
|
|
return &AuthMiddleware{
|
|
cache: cache,
|
|
}
|
|
}
|
|
|
|
type AuthMiddleware struct {
|
|
cache Cache
|
|
}
|
|
|
|
func (m *AuthMiddleware) Handler(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
token, err := TokenFromContext(r.Context())
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte("Invalid JWT token format"))
|
|
return
|
|
}
|
|
if token != nil {
|
|
ctx = context.WithValue(ctx, UserKey, token.Claims.(jwt.MapClaims)["sub"])
|
|
}
|
|
apiKey, err := ApiKeyFromContext(r.Context())
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte("Invalid API Key format"))
|
|
return
|
|
}
|
|
// Cache handles hash comparison internally
|
|
organization := m.cache.OrganizationByAPIKey(apiKey)
|
|
if organization != nil {
|
|
ctx = context.WithValue(ctx, OrganizationKey, *organization)
|
|
}
|
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
func UserFromContext(ctx context.Context) string {
|
|
if value := ctx.Value(UserKey); value != nil {
|
|
if u, ok := value.(string); ok {
|
|
return u
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func UserHasRole(ctx context.Context, role string) bool {
|
|
token, err := TokenFromContext(ctx)
|
|
if err != nil || token == nil {
|
|
return false
|
|
}
|
|
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Check the custom roles claim
|
|
rolesInterface, ok := claims["https://unbound.se/roles"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
roles, ok := rolesInterface.([]interface{})
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
for _, r := range roles {
|
|
if roleStr, ok := r.(string); ok && roleStr == role {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func OrganizationFromContext(ctx context.Context) string {
|
|
if value := ctx.Value(OrganizationKey); value != nil {
|
|
if u, ok := value.(domain.Organization); ok {
|
|
return u.ID.String()
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m *AuthMiddleware) Directive(ctx context.Context, _ interface{}, next graphql.Resolver, user *bool, organization *bool) (res interface{}, err error) {
|
|
userRequired := user != nil && *user
|
|
orgRequired := organization != nil && *organization
|
|
|
|
u := UserFromContext(ctx)
|
|
orgId := OrganizationFromContext(ctx)
|
|
|
|
fmt.Printf("[Auth Directive] userRequired=%v, orgRequired=%v, hasUser=%v, hasOrg=%v\n",
|
|
userRequired, orgRequired, u != "", orgId != "")
|
|
|
|
// If both are required, it means EITHER is acceptable (OR logic)
|
|
if userRequired && orgRequired {
|
|
if u == "" && orgId == "" {
|
|
fmt.Printf("[Auth Directive] REJECTED: Neither user nor organization available\n")
|
|
return nil, fmt.Errorf("authentication required: provide either user token or organization API key")
|
|
}
|
|
fmt.Printf("[Auth Directive] ACCEPTED: Has user=%v OR organization=%v\n", u != "", orgId != "")
|
|
return next(ctx)
|
|
}
|
|
|
|
// Only user required
|
|
if userRequired {
|
|
if u == "" {
|
|
fmt.Printf("[Auth Directive] REJECTED: No user available\n")
|
|
return nil, fmt.Errorf("no user available in request")
|
|
}
|
|
fmt.Printf("[Auth Directive] ACCEPTED: User authenticated\n")
|
|
}
|
|
|
|
// Only organization required
|
|
if orgRequired {
|
|
if orgId == "" {
|
|
fmt.Printf("[Auth Directive] REJECTED: No organization available\n")
|
|
return nil, fmt.Errorf("no organization available in request")
|
|
}
|
|
fmt.Printf("[Auth Directive] ACCEPTED: Organization authenticated\n")
|
|
}
|
|
|
|
return next(ctx)
|
|
}
|