Skip to content

Understanding the codependency of Authorization and Authentication

Yash Pal edited this page Jul 29, 2025 · 4 revisions

Backend Authorization Implementation Guide

Overview

This document explains how authorization (authz) works internally in the backend component, specifically focusing on the namespace handler implementation.

Core Components

1. Resource Policy Structure

We use a namespace handler to manage authorization through a Resource Policy. The policy is defined as:

type ResourcePolicy struct {
    Verb      ResourceVerb  // Actions (create, get, update, list, etc.)
    Group     string
    Version   string
    Kind      string
    Namespace string        // Associated with the k8s raw object
    Name      string
}

Key Points:

  • Only Verb and Namespace are important for our current authorization implementation
  • Verb defines the actions that can be performed on a resource
  • Namespace is associated with the Kubernetes raw object

Authorization Flow

1. Policy Creation and Authentication

// Create response envelope and send JSON response
responseEnvelope := &NamespaceListEnvelope{Data: namespaces}
a.dataResponse(w, r, responseEnvelope)

2. Authorization Check

if success := a.requireAuth(w, r, authPolicies); !success {
    return
}

This creates our custom auth Resource Policy instance and uses an if block to authenticate against the custom auth policies.

App Structure

The App struct serves as the main application container for the REST API server:

type App struct {
    Config               *config.EnvConfig          // Application configuration
    logger               *slog.Logger               // Structured logger
    repositories         *repositories.Repositories // Data access layer
    Scheme               *runtime.Scheme            // Kubernetes API scheme
    StrictYamlSerializer runtime.Serializer         // YAML serializer for K8s objects
    RequestAuthN         authenticator.Request      // Authentication handler
    RequestAuthZ         authorizer.Authorizer      // Authorization handler
}

What App Represents:

  • The central orchestrator containing all dependencies and configuration
  • The REST API server itself for workspace management
  • Handles HTTP requests and translates them into Kubernetes operations

This function verifies that the request is authenticated and authorized to take the actions specified by the given policies. By:

  • authenticating the request (extract user and groups from the request headers)
  • for each policy, check if the user is authorized to take the requested action
// requireAuth verifies that the request is authenticated and authorized to take the actions specified by the given policies.
// If this method returns false, the request has been handled and the caller should return immediately.
// If this method returns true, the request is authenticated and authorized to proceed.
// This method should only be called once per request.
func (a *App) requireAuth(w http.ResponseWriter, r *http.Request, policies []*auth.ResourcePolicy) bool {
	ctx := r.Context()

The DisableAuth return a boolean value

// if auth is disabled, allow the request to proceed
	if a.Config.DisableAuth {
		return true
	}

this returns a Response, bool, error to the request

// authenticate the request (extract user and groups from the request headers)
	res, ok, err := a.RequestAuthN.AuthenticateRequest(r)
	if err != nil {
		err = fmt.Errorf("failed to authenticate request: %w", err)
		a.serverErrorResponse(w, r, err)
		return false
	}
	if !ok {
		a.unauthorizedResponse(w, r) // returns 401
		return false
	}

Loop over each policy and check if the user is authorized to take the requested action

// for each policy, check if the user is authorized to take the requested action
	for _, policy := range policies {
		attributes := policy.AttributesFor(res.User)
		authorized, reason, err := a.RequestAuthZ.Authorize(ctx, attributes)
		if err != nil {
			err = fmt.Errorf("failed to authorize request for user %q: %w", res.User.GetName(), err)
			a.serverErrorResponse(w, r, err)
			return false
		}

		if authorized != authorizer.DecisionAllow {
			msg := fmt.Sprintf("authorization was denied for user %q", res.User.GetName())
			if reason != "" {
				msg = fmt.Sprintf("%s: %s", msg, reason)
			}
			a.forbiddenResponse(w, r, msg)
			return false
		}
	}

	return true

authorized, reason, err := a.RequestAuthZ.Authorize(ctx, attributes) We will go over this in detail This line performs the authorization check to determine if the authenticated user has permission to perform the requested action on a specific resource. Authorizer makes an authorization decision based on information gained by making zero or more calls to methods of the Attributes interface. It returns nil when an action is authorized, otherwise it returns an error. Authorize return values are: authorized Decision, reason string, err error

  1. Decision: a. DecisionAllow means that an authorizer decided to allow the action. b. DecisionDeny means that an authorizer decided to deny the action. c. DecisionNoOpinion means that an authorizer has no opinion on whether

  2. reason string: This provides a Human-readable explanation for the authorization decision Some examples are: "RBAC: allowed by RoleBinding 'workspace-users' of Role 'workspace-editor'" "RBAC: access denied" "User not in required group"

  3. error: Indicates if the authorization system encountered an error Examples: Network errors connecting to authorization service Configuration errors in RBAC rules Internal authorization system failures

Note: An error doesn't mean "denied" - it means the authorization system couldn't make a decision

Example Authorization Scenarios: Scenario 1: Successful Authorization

// User: [email protected], Groups: ["notebook-users"]
// Action: GET /api/v1/workspaces/my-namespace
authorized = authorizer.DecisionAllow
reason = "RBAC: allowed by RoleBinding 'notebook-users' of Role 'workspace-viewer'"
err = nil

Scenario 2: Access Denied

// User: [email protected], Groups: ["external-users"]  
// Action: DELETE /api/v1/workspaces/secure-namespace/important-workspace
authorized = authorizer.DecisionDeny
reason = "RBAC: access denied - user not in required group 'workspace-admins'"
err = nil

Scenario 3: Authorization System Error

// RBAC service is down or misconfigured
authorized = authorizer.DecisionNoOpinion
reason = ""
err = errors.New("failed to connect to authorization service")

Integration with Kubernetes RBAC: The authorization system typically integrates with Kubernetes RBAC, checking against:

ClusterRoles/Roles: Define what actions can be performed (like manager-role) ClusterRoleBindings/RoleBindings: Bind roles to users/groups (like manager-role and manager-rolebinding) ServiceAccount tokens: For service-to-service authorization (like controller-manager)

What attributes contains:

// AttributesFor returns an authorizer.Attributes which could be used with an authorizer.Authorizer to authorize the user for the resource policy.
func (p *ResourcePolicy) AttributesFor(u user.Info) authorizer.Attributes {
	return authorizer.AttributesRecord{
		User:            u,
		Verb:            string(p.Verb),
		Namespace:       p.Namespace,
		APIGroup:        p.Group,
		APIVersion:      p.Version,
		Resource:        p.Kind,
		Name:            p.Name,
		ResourceRequest: true,
	}
}

This authorization is heavily dependent on Kubernetes' apimachinery and related API server components.