353 lines
11 KiB
Go
353 lines
11 KiB
Go
package authorization
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/RyanCopley/skybridge/kms/internal/domain"
|
|
"github.com/RyanCopley/skybridge/kms/internal/errors"
|
|
)
|
|
|
|
// ResourceType represents different types of resources
|
|
type ResourceType string
|
|
|
|
const (
|
|
ResourceTypeApplication ResourceType = "application"
|
|
ResourceTypeToken ResourceType = "token"
|
|
ResourceTypePermission ResourceType = "permission"
|
|
ResourceTypeUser ResourceType = "user"
|
|
)
|
|
|
|
// Action represents different actions that can be performed
|
|
type Action string
|
|
|
|
const (
|
|
ActionRead Action = "read"
|
|
ActionWrite Action = "write"
|
|
ActionDelete Action = "delete"
|
|
ActionCreate Action = "create"
|
|
)
|
|
|
|
// AuthorizationContext holds context for authorization decisions
|
|
type AuthorizationContext struct {
|
|
UserID string
|
|
UserEmail string
|
|
ResourceType ResourceType
|
|
ResourceID string
|
|
Action Action
|
|
OwnerInfo *domain.Owner
|
|
}
|
|
|
|
// AuthorizationService provides role-based access control
|
|
type AuthorizationService struct {
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewAuthorizationService creates a new authorization service
|
|
func NewAuthorizationService(logger *zap.Logger) *AuthorizationService {
|
|
return &AuthorizationService{
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// AuthorizeResourceAccess checks if a user can perform an action on a resource
|
|
func (a *AuthorizationService) AuthorizeResourceAccess(ctx context.Context, authCtx *AuthorizationContext) error {
|
|
if authCtx == nil {
|
|
return errors.NewForbiddenError("Authorization context is required")
|
|
}
|
|
|
|
a.logger.Debug("Authorizing resource access",
|
|
zap.String("user_id", authCtx.UserID),
|
|
zap.String("resource_type", string(authCtx.ResourceType)),
|
|
zap.String("resource_id", authCtx.ResourceID),
|
|
zap.String("action", string(authCtx.Action)))
|
|
|
|
// Check if user is a system admin
|
|
if a.isSystemAdmin(authCtx.UserID) {
|
|
a.logger.Debug("System admin access granted", zap.String("user_id", authCtx.UserID))
|
|
return nil
|
|
}
|
|
|
|
// Check resource ownership
|
|
if authCtx.OwnerInfo != nil {
|
|
if a.isResourceOwner(authCtx, authCtx.OwnerInfo) {
|
|
a.logger.Debug("Resource owner access granted",
|
|
zap.String("user_id", authCtx.UserID),
|
|
zap.String("resource_id", authCtx.ResourceID))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Check specific resource-action combinations
|
|
switch authCtx.ResourceType {
|
|
case ResourceTypeApplication:
|
|
return a.authorizeApplicationAccess(authCtx)
|
|
case ResourceTypeToken:
|
|
return a.authorizeTokenAccess(authCtx)
|
|
case ResourceTypePermission:
|
|
return a.authorizePermissionAccess(authCtx)
|
|
case ResourceTypeUser:
|
|
return a.authorizeUserAccess(authCtx)
|
|
default:
|
|
return errors.NewForbiddenError(fmt.Sprintf("Unknown resource type: %s", authCtx.ResourceType))
|
|
}
|
|
}
|
|
|
|
// AuthorizeApplicationOwnership checks if a user owns an application
|
|
func (a *AuthorizationService) AuthorizeApplicationOwnership(userID string, app *domain.Application) error {
|
|
if app == nil {
|
|
return errors.NewValidationError("Application is required")
|
|
}
|
|
|
|
// System admins can access any application
|
|
if a.isSystemAdmin(userID) {
|
|
return nil
|
|
}
|
|
|
|
// Check if user is the owner
|
|
if a.isOwner(userID, &app.Owner) {
|
|
return nil
|
|
}
|
|
|
|
a.logger.Warn("Application ownership authorization failed",
|
|
zap.String("user_id", userID),
|
|
zap.String("app_id", app.AppID),
|
|
zap.String("owner_type", string(app.Owner.Type)),
|
|
zap.String("owner_name", app.Owner.Name))
|
|
|
|
return errors.NewForbiddenError("You do not have permission to access this application")
|
|
}
|
|
|
|
// AuthorizeTokenOwnership checks if a user owns a token
|
|
func (a *AuthorizationService) AuthorizeTokenOwnership(userID string, token interface{}) error {
|
|
// System admins can access any token
|
|
if a.isSystemAdmin(userID) {
|
|
return nil
|
|
}
|
|
|
|
// Extract owner information based on token type
|
|
var owner *domain.Owner
|
|
var tokenID string
|
|
|
|
switch t := token.(type) {
|
|
case *domain.StaticToken:
|
|
owner = &t.Owner
|
|
tokenID = t.ID.String()
|
|
case *domain.UserToken:
|
|
// For user tokens, the user ID should match
|
|
if t.UserID == userID {
|
|
return nil
|
|
}
|
|
tokenID = "user_token"
|
|
default:
|
|
return errors.NewValidationError("Unknown token type")
|
|
}
|
|
|
|
// Check ownership
|
|
if owner != nil && a.isOwner(userID, owner) {
|
|
return nil
|
|
}
|
|
|
|
a.logger.Warn("Token ownership authorization failed",
|
|
zap.String("user_id", userID),
|
|
zap.String("token_id", tokenID))
|
|
|
|
return errors.NewForbiddenError("You do not have permission to access this token")
|
|
}
|
|
|
|
// isSystemAdmin checks if a user is a system administrator
|
|
func (a *AuthorizationService) isSystemAdmin(userID string) bool {
|
|
// System admin users - this should be configurable
|
|
systemAdmins := []string{
|
|
"admin@example.com",
|
|
"system@internal.com",
|
|
}
|
|
|
|
for _, admin := range systemAdmins {
|
|
if userID == admin {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// isResourceOwner checks if the user is the owner of a resource
|
|
func (a *AuthorizationService) isResourceOwner(authCtx *AuthorizationContext, owner *domain.Owner) bool {
|
|
return a.isOwner(authCtx.UserID, owner)
|
|
}
|
|
|
|
// isOwner checks if a user is the owner based on owner information
|
|
func (a *AuthorizationService) isOwner(userID string, owner *domain.Owner) bool {
|
|
switch owner.Type {
|
|
case domain.OwnerTypeIndividual:
|
|
// For individual ownership, check if the user ID matches the owner name
|
|
return userID == owner.Name || userID == owner.Owner
|
|
case domain.OwnerTypeTeam:
|
|
// For team ownership, this would typically require a team membership check
|
|
// For now, we'll check if the user is the team owner
|
|
return userID == owner.Owner || a.isTeamMember(userID, owner.Name)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// isTeamMember checks if a user is a member of a team (placeholder implementation)
|
|
func (a *AuthorizationService) isTeamMember(userID, teamName string) bool {
|
|
// In a real implementation, this would check team membership in a database
|
|
// For now, we'll use a simple heuristic based on email domains
|
|
|
|
if !strings.Contains(userID, "@") {
|
|
return false
|
|
}
|
|
|
|
userDomain := strings.Split(userID, "@")[1]
|
|
teamDomain := strings.ToLower(teamName)
|
|
|
|
// Simple check: if team name looks like a domain and user's domain matches
|
|
if strings.Contains(teamDomain, ".") && strings.Contains(userDomain, teamDomain) {
|
|
return true
|
|
}
|
|
|
|
// Additional team membership logic would go here
|
|
return false
|
|
}
|
|
|
|
// authorizeApplicationAccess handles application-specific authorization
|
|
func (a *AuthorizationService) authorizeApplicationAccess(authCtx *AuthorizationContext) error {
|
|
switch authCtx.Action {
|
|
case ActionRead:
|
|
// Users can read applications they have some relationship with
|
|
// This could be expanded to check for shared access, etc.
|
|
return errors.NewForbiddenError("You do not have permission to read this application")
|
|
case ActionWrite:
|
|
// Only owners can modify applications
|
|
return errors.NewForbiddenError("You do not have permission to modify this application")
|
|
case ActionDelete:
|
|
// Only owners can delete applications
|
|
return errors.NewForbiddenError("You do not have permission to delete this application")
|
|
case ActionCreate:
|
|
// Most users can create applications (with rate limiting)
|
|
return nil
|
|
default:
|
|
return errors.NewForbiddenError(fmt.Sprintf("Unknown action: %s", authCtx.Action))
|
|
}
|
|
}
|
|
|
|
// authorizeTokenAccess handles token-specific authorization
|
|
func (a *AuthorizationService) authorizeTokenAccess(authCtx *AuthorizationContext) error {
|
|
switch authCtx.Action {
|
|
case ActionRead:
|
|
return errors.NewForbiddenError("You do not have permission to read this token")
|
|
case ActionWrite:
|
|
return errors.NewForbiddenError("You do not have permission to modify this token")
|
|
case ActionDelete:
|
|
return errors.NewForbiddenError("You do not have permission to delete this token")
|
|
case ActionCreate:
|
|
return errors.NewForbiddenError("You do not have permission to create tokens for this application")
|
|
default:
|
|
return errors.NewForbiddenError(fmt.Sprintf("Unknown action: %s", authCtx.Action))
|
|
}
|
|
}
|
|
|
|
// authorizePermissionAccess handles permission-specific authorization
|
|
func (a *AuthorizationService) authorizePermissionAccess(authCtx *AuthorizationContext) error {
|
|
switch authCtx.Action {
|
|
case ActionRead:
|
|
// Users can read permissions they have
|
|
return nil
|
|
case ActionWrite:
|
|
// Only admins can modify permissions
|
|
return errors.NewForbiddenError("You do not have permission to modify permissions")
|
|
case ActionDelete:
|
|
// Only admins can delete permissions
|
|
return errors.NewForbiddenError("You do not have permission to delete permissions")
|
|
case ActionCreate:
|
|
// Only admins can create permissions
|
|
return errors.NewForbiddenError("You do not have permission to create permissions")
|
|
default:
|
|
return errors.NewForbiddenError(fmt.Sprintf("Unknown action: %s", authCtx.Action))
|
|
}
|
|
}
|
|
|
|
// authorizeUserAccess handles user-specific authorization
|
|
func (a *AuthorizationService) authorizeUserAccess(authCtx *AuthorizationContext) error {
|
|
switch authCtx.Action {
|
|
case ActionRead:
|
|
// Users can read their own information
|
|
if authCtx.ResourceID == authCtx.UserID {
|
|
return nil
|
|
}
|
|
return errors.NewForbiddenError("You do not have permission to read this user's information")
|
|
case ActionWrite:
|
|
// Users can modify their own information
|
|
if authCtx.ResourceID == authCtx.UserID {
|
|
return nil
|
|
}
|
|
return errors.NewForbiddenError("You do not have permission to modify this user's information")
|
|
case ActionDelete:
|
|
// Users can delete their own account, admins can delete any
|
|
if authCtx.ResourceID == authCtx.UserID {
|
|
return nil
|
|
}
|
|
return errors.NewForbiddenError("You do not have permission to delete this user")
|
|
default:
|
|
return errors.NewForbiddenError(fmt.Sprintf("Unknown action: %s", authCtx.Action))
|
|
}
|
|
}
|
|
|
|
// AuthorizeListAccess checks if a user can list resources of a specific type
|
|
func (a *AuthorizationService) AuthorizeListAccess(ctx context.Context, userID string, resourceType ResourceType) error {
|
|
a.logger.Debug("Authorizing list access",
|
|
zap.String("user_id", userID),
|
|
zap.String("resource_type", string(resourceType)))
|
|
|
|
// System admins can list anything
|
|
if a.isSystemAdmin(userID) {
|
|
return nil
|
|
}
|
|
|
|
// For now, allow users to list their own resources
|
|
// This would be refined based on business requirements
|
|
switch resourceType {
|
|
case ResourceTypeApplication:
|
|
return nil // Users can list applications (filtered by ownership)
|
|
case ResourceTypeToken:
|
|
return nil // Users can list their own tokens
|
|
case ResourceTypePermission:
|
|
return nil // Users can list available permissions
|
|
case ResourceTypeUser:
|
|
// Only admins can list users
|
|
return errors.NewForbiddenError("You do not have permission to list users")
|
|
default:
|
|
return errors.NewForbiddenError(fmt.Sprintf("Unknown resource type: %s", resourceType))
|
|
}
|
|
}
|
|
|
|
// GetUserResourceFilter returns a filter for resources that a user can access
|
|
func (a *AuthorizationService) GetUserResourceFilter(userID string, resourceType ResourceType) map[string]interface{} {
|
|
filter := make(map[string]interface{})
|
|
|
|
// System admins see everything
|
|
if a.isSystemAdmin(userID) {
|
|
return filter // Empty filter means no restrictions
|
|
}
|
|
|
|
// Filter by ownership
|
|
switch resourceType {
|
|
case ResourceTypeApplication, ResourceTypeToken:
|
|
// Users can only see resources they own
|
|
filter["owner_email"] = userID
|
|
case ResourceTypePermission:
|
|
// Users can see all permissions (they're not user-specific)
|
|
return filter
|
|
case ResourceTypeUser:
|
|
// Users can only see themselves
|
|
filter["user_id"] = userID
|
|
}
|
|
|
|
return filter
|
|
} |