-
Notifications
You must be signed in to change notification settings - Fork 0
Understanding the codependency of Authorization and Authentication
This document explains how authorization (authz) works internally in the backend component, specifically focusing on the namespace handler implementation.
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
andNamespace
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
// Create response envelope and send JSON response
responseEnvelope := &NamespaceListEnvelope{Data: namespaces}
a.dataResponse(w, r, responseEnvelope)
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.
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
-
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
-
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"
-
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.