go first commit
This commit is contained in:
44
go_cloud/internal/audit/audit.go
Normal file
44
go_cloud/internal/audit/audit.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
func NewLogger(db *database.DB) *Logger {
|
||||
return &Logger{db: db}
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
UserID *uuid.UUID
|
||||
OrgID *uuid.UUID
|
||||
Action string
|
||||
Resource *string
|
||||
Success bool
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
func (l *Logger) Log(ctx context.Context, entry Entry) {
|
||||
metadataJSON, err := json.Marshal(entry.Metadata)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal audit metadata: %v", err)
|
||||
metadataJSON = []byte("{}")
|
||||
}
|
||||
|
||||
_, err = l.db.ExecContext(ctx, `
|
||||
INSERT INTO audit_logs (user_id, org_id, action, resource, success, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`, entry.UserID, entry.OrgID, entry.Action, entry.Resource, entry.Success, metadataJSON)
|
||||
if err != nil {
|
||||
log.Printf("Failed to log audit entry: %v", err)
|
||||
}
|
||||
}
|
||||
96
go_cloud/internal/auth/auth.go
Normal file
96
go_cloud/internal/auth/auth.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/config"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
provider *oidc.Provider
|
||||
oauth2Config oauth2.Config
|
||||
db *database.DB // Assume we have a DB wrapper
|
||||
}
|
||||
|
||||
func NewService(cfg *config.Config, db *database.DB) (*Service, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
provider, err := oidc.NewProvider(ctx, cfg.OIDCIssuerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get OIDC provider: %w", err)
|
||||
}
|
||||
|
||||
oauth2Config := oauth2.Config{
|
||||
ClientID: cfg.OIDCClientID,
|
||||
ClientSecret: cfg.OIDCClientSecret,
|
||||
RedirectURL: cfg.OIDCRedirectURL, // Add to config
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
}
|
||||
|
||||
return &Service{
|
||||
provider: provider,
|
||||
oauth2Config: oauth2Config,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) LoginURL(state string) string {
|
||||
return s.oauth2Config.AuthCodeURL(state)
|
||||
}
|
||||
|
||||
func (s *Service) HandleCallback(ctx context.Context, code, state string) (*database.User, *database.Session, error) {
|
||||
oauth2Token, err := s.oauth2Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to exchange code: %w", err)
|
||||
}
|
||||
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("no id_token in token response")
|
||||
}
|
||||
|
||||
idToken, err := s.provider.Verifier(&oidc.Config{ClientID: s.oauth2Config.ClientID}).Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to verify ID token: %w", err)
|
||||
}
|
||||
|
||||
var claims struct {
|
||||
Sub string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse claims: %w", err)
|
||||
}
|
||||
|
||||
user, err := s.db.GetOrCreateUser(ctx, claims.Sub, claims.Email, claims.Name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
session, err := s.db.CreateSession(ctx, user.ID, time.Now().Add(15*time.Minute))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return user, session, nil
|
||||
}
|
||||
|
||||
// GenerateState generates a secure random state string
|
||||
func GenerateState() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
29
go_cloud/internal/auth/auth_test.go
Normal file
29
go_cloud/internal/auth/auth_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateState(t *testing.T) {
|
||||
state1, err := GenerateState()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state2, err := GenerateState()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if state1 == state2 {
|
||||
t.Error("States should be unique")
|
||||
}
|
||||
if len(state1) == 0 {
|
||||
t.Error("State should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
// Mock db
|
||||
// service, err := NewService(cfg, db)
|
||||
// TODO: Mock database for full test
|
||||
t.Skip("Requires database mock")
|
||||
}
|
||||
34
go_cloud/internal/config/config.go
Normal file
34
go_cloud/internal/config/config.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ServerAddr string
|
||||
DatabaseURL string
|
||||
OIDCIssuerURL string
|
||||
OIDCRedirectURL string
|
||||
OIDCClientID string
|
||||
OIDCClientSecret string
|
||||
JWTSecret string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
ServerAddr: getEnv("SERVER_ADDR", ":8080"),
|
||||
DatabaseURL: os.Getenv("DATABASE_URL"),
|
||||
OIDCIssuerURL: os.Getenv("OIDC_ISSUER_URL"),
|
||||
OIDCRedirectURL: os.Getenv("OIDC_REDIRECT_URL"),
|
||||
OIDCClientID: os.Getenv("OIDC_CLIENT_ID"),
|
||||
OIDCClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
|
||||
JWTSecret: os.Getenv("JWT_SECRET"),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultVal string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
29
go_cloud/internal/database/database.go
Normal file
29
go_cloud/internal/database/database.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/config"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
func Connect(cfg *config.Config) (*sql.DB, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
pool, err := pgxpool.New(ctx, cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create pool: %w", err)
|
||||
}
|
||||
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
db := stdlib.OpenDBFromPool(pool)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
124
go_cloud/internal/database/db.go
Normal file
124
go_cloud/internal/database/db.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func New(db *sql.DB) *DB {
|
||||
return &DB{DB: db}
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uuid.UUID
|
||||
Email string
|
||||
DisplayName string
|
||||
CreatedAt time.Time
|
||||
LastLoginAt *time.Time
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
ID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
ExpiresAt time.Time
|
||||
RevokedAt *time.Time
|
||||
}
|
||||
|
||||
type Organization struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
Slug string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Membership struct {
|
||||
UserID uuid.UUID
|
||||
OrgID uuid.UUID
|
||||
Role string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (db *DB) GetOrCreateUser(ctx context.Context, sub, email, name string) (*User, error) {
|
||||
var user User
|
||||
err := db.QueryRowContext(ctx, `
|
||||
INSERT INTO users (id, email, display_name)
|
||||
VALUES (gen_random_uuid(), $1, $2)
|
||||
ON CONFLICT (email) DO UPDATE SET
|
||||
display_name = EXCLUDED.display_name,
|
||||
last_login_at = NOW()
|
||||
RETURNING id, email, display_name, created_at, last_login_at
|
||||
`, email, name).Scan(&user.ID, &user.Email, &user.DisplayName, &user.CreatedAt, &user.LastLoginAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (db *DB) CreateSession(ctx context.Context, userID uuid.UUID, expiresAt time.Time) (*Session, error) {
|
||||
var session Session
|
||||
err := db.QueryRowContext(ctx, `
|
||||
INSERT INTO sessions (user_id, expires_at)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, user_id, expires_at, revoked_at
|
||||
`, userID, expiresAt).Scan(&session.ID, &session.UserID, &session.ExpiresAt, &session.RevokedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetSession(ctx context.Context, sessionID uuid.UUID) (*Session, error) {
|
||||
var session Session
|
||||
err := db.QueryRowContext(ctx, `
|
||||
SELECT id, user_id, expires_at, revoked_at
|
||||
FROM sessions
|
||||
WHERE id = $1
|
||||
`, sessionID).Scan(&session.ID, &session.UserID, &session.ExpiresAt, &session.RevokedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetUserOrganizations(ctx context.Context, userID uuid.UUID) ([]Organization, error) {
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT o.id, o.name, o.slug, o.created_at
|
||||
FROM organizations o
|
||||
JOIN memberships m ON o.id = m.org_id
|
||||
WHERE m.user_id = $1
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var orgs []Organization
|
||||
for rows.Next() {
|
||||
var org Organization
|
||||
if err := rows.Scan(&org.ID, &org.Name, &org.Slug, &org.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orgs = append(orgs, org)
|
||||
}
|
||||
return orgs, rows.Err()
|
||||
}
|
||||
|
||||
func (db *DB) GetUserMembership(ctx context.Context, userID, orgID uuid.UUID) (*Membership, error) {
|
||||
var membership Membership
|
||||
err := db.QueryRowContext(ctx, `
|
||||
SELECT user_id, org_id, role, created_at
|
||||
FROM memberships
|
||||
WHERE user_id = $1 AND org_id = $2
|
||||
`, userID, orgID).Scan(&membership.UserID, &membership.OrgID, &membership.Role, &membership.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &membership, nil
|
||||
}
|
||||
93
go_cloud/internal/http/routes.go
Normal file
93
go_cloud/internal/http/routes.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/audit"
|
||||
"go.b0esche.cloud/backend/internal/auth"
|
||||
"go.b0esche.cloud/backend/internal/config"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
"go.b0esche.cloud/backend/internal/middleware"
|
||||
"go.b0esche.cloud/backend/pkg/jwt"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, authService *auth.Service, auditLogger *audit.Logger) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Global middleware
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.RateLimit)
|
||||
|
||||
// Health check
|
||||
r.Get("/health", healthHandler)
|
||||
|
||||
// Auth routes (no auth required)
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Get("/login", func(w http.ResponseWriter, req *http.Request) {
|
||||
authLoginHandler(w, req, authService)
|
||||
})
|
||||
r.Get("/callback", func(w http.ResponseWriter, req *http.Request) {
|
||||
authCallbackHandler(w, req, cfg, authService, jwtManager, auditLogger)
|
||||
})
|
||||
})
|
||||
|
||||
// Auth middleware for protected routes
|
||||
r.Use(middleware.Auth(jwtManager, db))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
func authLoginHandler(w http.ResponseWriter, r *http.Request, authService *auth.Service) {
|
||||
state, err := auth.GenerateState()
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Store state securely (e.g., in session or cache)
|
||||
|
||||
url := authService.LoginURL(state)
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
|
||||
func authCallbackHandler(w http.ResponseWriter, r *http.Request, cfg *config.Config, authService *auth.Service, jwtManager *jwt.Manager, auditLogger *audit.Logger) {
|
||||
code := r.URL.Query().Get("code")
|
||||
state := r.URL.Query().Get("state")
|
||||
|
||||
// TODO: Validate state
|
||||
|
||||
user, session, err := authService.HandleCallback(r.Context(), code, state)
|
||||
if err != nil {
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
Action: "login",
|
||||
Success: false,
|
||||
Metadata: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
http.Error(w, "Authentication failed", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwtManager.Generate(user.Email, []string{}, session.ID.String()) // Orgs not yet
|
||||
if err != nil {
|
||||
http.Error(w, "Token generation failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &user.ID,
|
||||
Action: "login",
|
||||
Success: true,
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"token": "` + token + `"}`))
|
||||
}
|
||||
26
go_cloud/internal/http/server.go
Normal file
26
go_cloud/internal/http/server.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/audit"
|
||||
"go.b0esche.cloud/backend/internal/auth"
|
||||
"go.b0esche.cloud/backend/internal/config"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
"go.b0esche.cloud/backend/pkg/jwt"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*http.Server
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, authService *auth.Service, auditLogger *audit.Logger) *Server {
|
||||
r := NewRouter(cfg, db, jwtManager, authService, auditLogger)
|
||||
|
||||
return &Server{
|
||||
Server: &http.Server{
|
||||
Addr: cfg.ServerAddr,
|
||||
Handler: r,
|
||||
},
|
||||
}
|
||||
}
|
||||
123
go_cloud/internal/middleware/middleware.go
Normal file
123
go_cloud/internal/middleware/middleware.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/audit"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
"go.b0esche.cloud/backend/internal/org"
|
||||
"go.b0esche.cloud/backend/internal/permission"
|
||||
"go.b0esche.cloud/backend/pkg/jwt"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var RequestID = middleware.RequestID
|
||||
var Logger = middleware.Logger
|
||||
var Recoverer = middleware.Recoverer
|
||||
|
||||
// TODO: Implement rate limiter
|
||||
var RateLimit = func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Basic rate limiting logic here
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
userKey contextKey = "user"
|
||||
sessionKey contextKey = "session"
|
||||
orgKey contextKey = "org"
|
||||
)
|
||||
|
||||
// Auth middleware
|
||||
func Auth(jwtManager *jwt.Manager, db *database.DB) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
claims, session, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db)
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), userKey, claims.UserID)
|
||||
ctx = context.WithValue(ctx, sessionKey, session)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Org middleware
|
||||
func Org(db *database.DB, auditLogger *audit.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userIDStr := r.Context().Value(userKey).(string)
|
||||
userID, _ := uuid.Parse(userIDStr)
|
||||
|
||||
orgIDStr := r.Header.Get("X-Org-ID")
|
||||
if orgIDStr == "" {
|
||||
orgIDStr = chi.URLParam(r, "orgId")
|
||||
}
|
||||
orgID, err := uuid.Parse(orgIDStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid org ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = org.CheckMembership(r.Context(), db, userID, orgID)
|
||||
if err != nil {
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
Action: "org_access",
|
||||
Success: false,
|
||||
Metadata: map[string]interface{}{"org_id": orgID, "error": err.Error()},
|
||||
})
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), orgKey, orgID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Permission middleware
|
||||
func Permission(db *database.DB, auditLogger *audit.Logger, perm permission.Permission) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userIDStr := r.Context().Value(userKey).(string)
|
||||
userID, _ := uuid.Parse(userIDStr)
|
||||
orgID := r.Context().Value(orgKey).(uuid.UUID)
|
||||
|
||||
hasPerm, err := permission.HasPermission(r.Context(), db, userID, orgID, perm)
|
||||
if err != nil || !hasPerm {
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
OrgID: &orgID,
|
||||
Action: "permission_check",
|
||||
Resource: &[]string{string(perm)}[0],
|
||||
Success: false,
|
||||
Metadata: map[string]interface{}{"permission": perm},
|
||||
})
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
23
go_cloud/internal/org/org.go
Normal file
23
go_cloud/internal/org/org.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ResolveUserOrgs returns the organizations a user belongs to
|
||||
func ResolveUserOrgs(ctx context.Context, db *database.DB, userID uuid.UUID) ([]database.Organization, error) {
|
||||
return db.GetUserOrganizations(ctx, userID)
|
||||
}
|
||||
|
||||
// CheckMembership checks if user is member of org and returns role
|
||||
func CheckMembership(ctx context.Context, db *database.DB, userID, orgID uuid.UUID) (string, error) {
|
||||
membership, err := db.GetUserMembership(ctx, userID, orgID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return membership.Role, nil
|
||||
}
|
||||
48
go_cloud/internal/permission/permission.go
Normal file
48
go_cloud/internal/permission/permission.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package permission
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Permission string
|
||||
|
||||
const (
|
||||
FileRead Permission = "file.read"
|
||||
FileWrite Permission = "file.write"
|
||||
FileDelete Permission = "file.delete"
|
||||
DocumentView Permission = "document.view"
|
||||
DocumentEdit Permission = "document.edit"
|
||||
OrgManage Permission = "org.manage"
|
||||
)
|
||||
|
||||
var rolePermissions = map[string][]Permission{
|
||||
"owner": {FileRead, FileWrite, FileDelete, DocumentView, DocumentEdit, OrgManage},
|
||||
"admin": {FileRead, FileWrite, FileDelete, DocumentView, DocumentEdit},
|
||||
"editor": {FileRead, FileWrite, DocumentView, DocumentEdit},
|
||||
"viewer": {FileRead, DocumentView},
|
||||
}
|
||||
|
||||
// HasPermission checks if user has permission in org
|
||||
func HasPermission(ctx context.Context, db *database.DB, userID, orgID uuid.UUID, perm Permission) (bool, error) {
|
||||
membership, err := db.GetUserMembership(ctx, userID, orgID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
perms, ok := rolePermissions[membership.Role]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unknown role: %s", membership.Role)
|
||||
}
|
||||
|
||||
for _, p := range perms {
|
||||
if p == perm {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user