Protect an endpoint
Protect your API endpoints by checking user permissions before granting access. Youâll define permissions in KSL, initialize the SDK, and add permission checks to your middleware.
Prerequisites
Section titled âPrerequisitesâ- Basic understanding of the Kessel Schema Language (KSL) for defining resources with permissions
Define your service permissions
Section titled âDefine your service permissionsâThe base RBAC schema (rbac.ksl) is already deployed to Kessel, so you only need to register your serviceâs permissions.
Register workspace-level permissions
Section titled âRegister workspace-level permissionsâCreate a schema file for your service. For example, myservice.ksl:
version 0.2namespace myservice
import rbac
// View permission for integrations - allows reading integration configurations@rbac.add_unified_permission(app:'myservice', resource:'integration', verb:'view');
// Edit permission for integrations - allows creating/updating integration configurations@rbac.add_unified_permission(app:'myservice', resource:'integration', verb:'edit');In this schema:
import rbacbrings in the existing RBAC workspace hierarchyappis your service name (myservice)resourceis the resource type within your service (integration)verbis the actionâtypicallyvieworedit- This creates permission names like
myservice_integration_viewandmyservice_integration_editfor Check API calls
These permissions can be checked at the workspace level and are automatically inherited through the workspace hierarchy.
Initialize the SDK client
Section titled âInitialize the SDK clientâThe SDK handles authentication and TLS for you.
Install the SDK:
go get github.com/project-kessel/kessel-sdk-goCreate the client:
package main
import ( "context" "log" "os"
"github.com/project-kessel/kessel-sdk-go/kessel/inventory/v1beta2")
func main() { ctx := context.Background()
// Create client using environment configuration inventoryClient, conn, err := v1beta2.NewClientBuilder(os.Getenv("KESSEL_ENDPOINT")). Insecure(). // For local dev - use authenticated connection in production Build() if err != nil { log.Fatal("Failed to create client:", err) } defer conn.Close()
// Use inventoryClient for permission checks}Install the SDK:
pip install kessel-sdk-pyCreate the client:
from kessel.inventory.v1beta2 import ClientBuilderimport os
KESSEL_ENDPOINT = os.getenv("KESSEL_ENDPOINT", "localhost:9000")
# Create client using environment configurationstub, channel = ClientBuilder(KESSEL_ENDPOINT).insecure().build()
with channel: # Use stub for permission checks passInstall the SDK:
npm install @project-kessel/kessel-sdkCreate the client:
import { ClientBuilder } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2";
// Create client using environment configurationconst client = new ClientBuilder(process.env.KESSEL_ENDPOINT) .insecure() // For local dev - use authenticated connection in production .buildAsync();
// Use client for permission checksInstall the SDK:
gem install kessel-sdkCreate the client:
require 'kessel-sdk'
include Kessel::Inventory::V1beta2include Kessel::GRPC
# Create client using environment configurationclient = KesselInventoryService::ClientBuilder .new(ENV.fetch('KESSEL_ENDPOINT', 'localhost:9000')) .insecure # For local dev - use authenticated connection in production .build
# Use client for permission checksAdd the SDK dependency to your pom.xml:
<dependency> <groupId>org.project-kessel</groupId> <artifactId>kessel-sdk-java</artifactId> <version>LATEST</version></dependency>Create the client:
import com.nimbusds.jose.util.Pair;import io.grpc.ManagedChannel;import org.project_kessel.api.inventory.v1beta2.*;import static org.project_kessel.api.inventory.v1beta2.KesselInventoryServiceGrpc.KesselInventoryServiceBlockingStub;
public class KesselClient { public static void main(String[] args) { String kesselEndpoint = System.getenv() .getOrDefault("KESSEL_ENDPOINT", "localhost:9000");
// Create client using environment configuration Pair<KesselInventoryServiceBlockingStub, ManagedChannel> clientAndChannel = new ClientBuilder(kesselEndpoint) .insecure() // For local dev - use authenticated connection in production .build();
KesselInventoryServiceBlockingStub client = clientAndChannel.getLeft(); ManagedChannel channel = clientAndChannel.getRight();
try { // Use client for permission checks } finally { channel.shutdown(); } }}Implement permission checks
Section titled âImplement permission checksâUse Check() for reads and CheckForUpdate() for writes. The difference is consistency: reads can tolerate slight staleness, writes need the latest state.
When to use Check vs CheckForUpdate
Section titled âWhen to use Check vs CheckForUpdateâ| Method | Consistency | Use For | Example Operations |
|---|---|---|---|
| Check() | Eventually consistent (configurable) | Read operations, UI visibility decisions, standard authorization | GET requests, list filtering, showing/hiding UI elements |
| CheckForUpdate() | Strongly consistent (always) | Write operations, sensitive operations | PUT/POST/DELETE requests, resource modification, credential access |
Check: Read operations
Section titled âCheck: Read operationsâUse Check() for reads. Eventual consistency is fine hereâusers wonât notice a few hundred milliseconds of staleness.
import ( "context" "log"
"github.com/project-kessel/kessel-sdk-go/kessel/inventory/v1beta2" v2 "github.com/project-kessel/kessel-sdk-go/kessel/rbac/v2")
func checkReadPermission(ctx context.Context, client v1beta2.KesselInventoryServiceClient, userID, workspaceID string) (bool, error) { checkRequest := &v1beta2.CheckRequest{ Object: v2.WorkspaceResource(workspaceID), Relation: "myservice_integration_view", Subject: v2.PrincipalSubject(userID, "redhat"), }
response, err := client.Check(ctx, checkRequest) if err != nil { return false, err }
return response.Allowed == v1beta2.Allowed_ALLOWED_TRUE, nil}from kessel.inventory.v1beta2 import check_request_pb2, allowed_pb2from kessel.rbac.v2 import principal_subject, workspace_resource
def check_read_permission(stub, user_id: str, workspace_id: str) -> bool: check_request = check_request_pb2.CheckRequest( object=workspace_resource(workspace_id), relation="myservice_integration_view", subject=principal_subject(id=user_id, domain="redhat"), )
response = stub.Check(check_request) return response.allowed == allowed_pb2.ALLOWED_TRUEimport { ClientBuilder } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2";import { Allowed } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/allowed";import { principalSubject, workspaceResource } from "@project-kessel/kessel-sdk/kessel/rbac/v2";
async function checkReadPermission(client, userId, workspaceId) { const checkRequest = { object: workspaceResource(workspaceId), relation: "myservice_integration_view", subject: principalSubject(userId, "redhat"), };
try { const response = await client.check(checkRequest); return response.allowed === Allowed.ALLOWED_TRUE; } catch (error) { console.error("Check failed:", error); return false; }}require 'kessel-sdk'
include Kessel::Inventory::V1beta2include Kessel::RBAC::V2
def check_read_permission(client, user_id, workspace_id) check_request = CheckRequest.new( object: workspace_resource(workspace_id), relation: "myservice_integration_view", subject: principal_subject(user_id, "redhat") )
response = client.check(check_request) response.allowed == :ALLOWED_TRUEendimport org.project_kessel.api.inventory.v1beta2.*;import org.project_kessel.api.rbac.v2.Utils;import static org.project_kessel.api.inventory.v1beta2.KesselInventoryServiceGrpc.KesselInventoryServiceBlockingStub;
public boolean checkReadPermission(KesselInventoryServiceBlockingStub client, String userId, String workspaceId) { CheckRequest checkRequest = CheckRequest.newBuilder() .setObject(Utils.workspaceResource(workspaceId)) .setRelation("myservice_integration_view") .setSubject(Utils.principalSubject(userId, "redhat")) .build();
CheckResponse response = client.check(checkRequest); return response.getAllowed() == Allowed.ALLOWED_TRUE;}CheckForUpdate: Write operations
Section titled âCheckForUpdate: Write operationsâUse CheckForUpdate() for writes. This ensures youâre checking against the latest permission state.
func checkWritePermission(ctx context.Context, client v1beta2.KesselInventoryServiceClient, userID, workspaceID string) (bool, error) { checkRequest := &v1beta2.CheckForUpdateRequest{ Object: v2.WorkspaceResource(workspaceID), Relation: "myservice_integration_edit", Subject: v2.PrincipalSubject(userID, "redhat"), }
response, err := client.CheckForUpdate(ctx, checkRequest) if err != nil { return false, err }
return response.Allowed == v1beta2.Allowed_ALLOWED_TRUE, nil}from kessel.inventory.v1beta2 import ( check_for_update_request_pb2, allowed_pb2,)from kessel.rbac.v2 import principal_subject, workspace_resource
def check_write_permission(stub, user_id: str, workspace_id: str) -> bool: check_request = check_for_update_request_pb2.CheckForUpdateRequest( object=workspace_resource(workspace_id), relation="myservice_integration_edit", subject=principal_subject(id=user_id, domain="redhat"), )
response = stub.CheckForUpdate(check_request) return response.allowed == allowed_pb2.ALLOWED_TRUEimport { ClientBuilder } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2";import { Allowed } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/allowed";import { principalSubject, workspaceResource } from "@project-kessel/kessel-sdk/kessel/rbac/v2";
async function checkWritePermission(client, userId, workspaceId) { const checkRequest = { object: workspaceResource(workspaceId), relation: "myservice_integration_edit", subject: principalSubject(userId, "redhat"), };
try { const response = await client.checkForUpdate(checkRequest); return response.allowed === Allowed.ALLOWED_TRUE; } catch (error) { console.error("CheckForUpdate failed:", error); return false; }}require 'kessel-sdk'
include Kessel::Inventory::V1beta2include Kessel::RBAC::V2
def check_write_permission(client, user_id, workspace_id) check_request = CheckForUpdateRequest.new( object: workspace_resource(workspace_id), relation: "myservice_integration_edit", subject: principal_subject(user_id, "redhat") )
response = client.check_for_update(check_request) response.allowed == :ALLOWED_TRUEendimport org.project_kessel.api.inventory.v1beta2.*;import org.project_kessel.api.rbac.v2.Utils;import static org.project_kessel.api.inventory.v1beta2.KesselInventoryServiceGrpc.KesselInventoryServiceBlockingStub;
public boolean checkWritePermission(KesselInventoryServiceBlockingStub client, String userId, String workspaceId) { CheckForUpdateRequest checkRequest = CheckForUpdateRequest.newBuilder() .setObject(Utils.workspaceResource(workspaceId)) .setRelation("myservice_integration_edit") .setSubject(Utils.principalSubject(userId, "redhat")) .build();
CheckForUpdateResponse response = client.checkForUpdate(checkRequest); return response.getAllowed() == Allowed.ALLOWED_TRUE;}Integrate with middleware
Section titled âIntegrate with middlewareâAdd permission checks to your frameworkâs middleware:
Client initialization:
# Initialize Kessel clientstub, channel = ClientBuilder("localhost:9000").insecure().build()func init() { client, conn, err := v1beta2.NewClientBuilder(os.Getenv("KESSEL_ENDPOINT")). Insecure(). Build() if err != nil { panic(err) } kesselClient = client // Note: In production, handle conn.Close() properly}const client = new ClientBuilder("localhost:9000") .insecure() .build();# Initialize Kessel clientclient = Kessel::Inventory::V1beta2::ClientBuilder .new("localhost:9000") .insecure .buildpublic class ProtectEndpointExample { private final KesselInventoryServiceBlockingStub kesselClient;
public ProtectEndpointExample() { this.kesselClient = new ClientBuilder("localhost:9000") .insecure() .build(); }Middleware implementation:
def require_permission(relation: str): """Decorator to require permission check before executing endpoint.""" def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): # Extract user from request headers user_id = request.headers.get("X-User-ID") user_domain = request.headers.get("X-User-Domain", "redhat")
# Extract workspace from header workspace_id = request.headers.get("X-Workspace-ID")
if not user_id or not workspace_id: abort(400, "Missing required headers")
try: # Build check request check_req = check_request_pb2.CheckRequest( subject=principal_subject(user_id, user_domain), relation=relation, object=workspace_resource(workspace_id), )
# Perform permission check response = stub.Check(check_req)
if response.allowed != allowed_pb2.ALLOWED_TRUE: abort(403, f"Permission denied for relation '{relation}'")
# Permission granted, proceed return f(*args, **kwargs)
except ConnectError as e: abort(500, f"Permission check failed: {e.message}")
return decorated_function return decorator// RequirePermission middleware checks if user has required permissionfunc RequirePermission(relation string, next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Extract user from header (from your auth middleware) userID := r.Header.Get("X-User-ID") if userID == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return }
// Extract workspace ID from header or request context workspaceID := r.Header.Get("X-Workspace-ID") if workspaceID == "" { http.Error(w, "Missing workspace", http.StatusBadRequest) return }
// Check permission checkReq := &v1beta2.CheckRequest{ Object: v2.WorkspaceResource(workspaceID), Relation: relation, Subject: v2.PrincipalSubject(userID, "redhat"), }
response, err := kesselClient.Check(context.Background(), checkReq) if err != nil { http.Error(w, "Permission check failed", http.StatusInternalServerError) return }
if response.Allowed != v1beta2.Allowed_ALLOWED_TRUE { http.Error(w, "Forbidden", http.StatusForbidden) return }
// Permission granted next(w, r) }}// Middleware to check permissionfunction requirePermission(relation: string) { return async (req: Request, res: Response, next: NextFunction) => { const userId = req.headers["x-user-id"] as string; const workspaceId = req.headers["x-workspace-id"] as string;
if (!userId || !workspaceId) { return res.status(400).json({ error: "Missing required headers" }); }
try { const checkRequest = { object: workspaceResource(workspaceId), relation, subject: principalSubject(userId, "redhat"), };
const response = await client.check(checkRequest);
if (response.allowed !== Allowed.ALLOWED_TRUE) { return res.status(403).json({ error: "Forbidden" }); }
next(); } catch (error) { console.error("Check failed:", error); return res.status(500).json({ error: "Permission check failed" }); } };}# Helper to check permissionsdef check_permission(client, relation) user_id = request.env['HTTP_X_USER_ID'] workspace_id = request.env['HTTP_X_WORKSPACE_ID']
halt 400, { error: 'Missing required headers' }.to_json if user_id.nil? || workspace_id.nil?
check_request = Kessel::Inventory::V1beta2::CheckRequest.new( object: workspace_resource(workspace_id), relation: relation, subject: principal_subject(user_id, "redhat") )
begin response = client.check(check_request) unless response.allowed == Kessel::Inventory::V1beta2::Allowed::ALLOWED_TRUE halt 403, { error: 'Forbidden' }.to_json end rescue StandardError => e logger.error "Check failed: #{e.message}" halt 500, { error: 'Permission check failed' }.to_json endend// Servlet filter for permission checkingpublic class PermissionFilter implements Filter { private final String relation;
public PermissionFilter(String relation) { this.relation = relation; }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response;
String userId = httpRequest.getHeader("X-User-ID"); String workspaceId = httpRequest.getHeader("X-Workspace-ID");
if (userId == null || workspaceId == null) { httpResponse.sendError(HttpStatus.BAD_REQUEST.value(), "Missing required headers"); return; }
CheckRequest checkRequest = CheckRequest.newBuilder() .setObject(Utils.workspaceResource(workspaceId)) .setRelation(relation) .setSubject(Utils.principalSubject(userId, "redhat")) .build();
try { CheckResponse checkResponse = kesselClient.check(checkRequest);
if (checkResponse.getAllowed() != Allowed.ALLOWED_TRUE) { httpResponse.sendError(HttpStatus.FORBIDDEN.value(), "Forbidden"); return; }
chain.doFilter(request, response); } catch (Exception e) { httpResponse.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Permission check failed"); } }}Using the middleware:
@app.route("/integrations/<integration_id>", methods=["GET"])@require_permission(relation="myservice_integration_view")def get_integration(integration_id): return {"integration_id": integration_id, "name": "Example Integration"}
@app.route("/integrations/<integration_id>", methods=["PUT"])@require_permission(relation="myservice_integration_edit")def update_integration(integration_id): return {"status": "updated"}
with channel: app.run(debug=True)func main() { http.HandleFunc("GET /integrations/{id}", RequirePermission("myservice_integration_view", getIntegrationHandler)) http.HandleFunc("PUT /integrations/{id}", RequirePermission("myservice_integration_edit", updateIntegrationHandler))
http.ListenAndServe(":8080", nil)}
func getIntegrationHandler(w http.ResponseWriter, r *http.Request) { // Handle GET request}
func updateIntegrationHandler(w http.ResponseWriter, r *http.Request) { // Handle PUT request}const app = express();
app.get( "/integrations/:id", requirePermission("myservice_integration_view"), (req: Request, res: Response) => { res.json({ integration_id: req.params.id }); });
app.put( "/integrations/:id", requirePermission("myservice_integration_edit"), (req: Request, res: Response) => { res.json({ status: "updated" }); });
app.listen(8080);get '/integrations/:id' do check_permission(client, "myservice_integration_view") { integration_id: params[:id] }.to_jsonend
put '/integrations/:id' do check_permission(client, "myservice_integration_edit") { status: 'updated' }.to_jsonend@RestController@RequestMapping("/integrations")public class IntegrationController {
@GetMapping("/{id}") public ResponseEntity<?> getIntegration(@PathVariable String id) { return ResponseEntity.ok(Map.of("integration_id", id)); }
@PutMapping("/{id}") public ResponseEntity<?> updateIntegration(@PathVariable String id) { return ResponseEntity.ok(Map.of("status", "updated")); }}Handle errors
Section titled âHandle errorsâIf Kessel is unavailable, decide whether to fail closed (deny access) or fail open (allow access). Fail closed is safer for most cases.
func checkPermissionSafe(ctx context.Context, client v1beta2.KesselInventoryServiceClient, req *v1beta2.CheckRequest) bool { response, err := client.Check(ctx, req) if err != nil { // Log the error for monitoring log.Printf("Permission check failed: %v", err)
// Fail closed: deny access when check fails return false }
return response.Allowed == v1beta2.Allowed_ALLOWED_TRUE}from connectrpc.errors import ConnectErrorfrom kessel.inventory.v1beta2 import allowed_pb2
def check_permission_safe(stub, check_request): """Check permission with error handling.""" try: response = stub.Check(check_request) return response.allowed == allowed_pb2.ALLOWED_TRUE except ConnectError as e: # Log the error for monitoring print(f"Permission check failed: {e.message}")
# Fail closed: deny access when check fails return Falseasync function checkPermissionSafe(client, checkRequest) { try { const response = await client.check(checkRequest); return response.allowed === Allowed.ALLOWED_TRUE; } catch (error) { // Log the error for monitoring console.error("Permission check failed:", error);
// Fail closed: deny access when check fails return false; }}def check_permission_safe(client, check_request) response = client.check(check_request) response.allowed == :ALLOWED_TRUErescue StandardError => e # Log the error for monitoring puts "Permission check failed: #{e.message}"
# Fail closed: deny access when check fails falseendimport io.grpc.StatusRuntimeException;
public boolean checkPermissionSafe(KesselInventoryServiceBlockingStub client, CheckRequest checkRequest) { try { CheckResponse response = client.check(checkRequest); return response.getAllowed() == Allowed.ALLOWED_TRUE; } catch (StatusRuntimeException e) { // Log the error for monitoring System.err.println("Permission check failed: " + e.getMessage());
// Fail closed: deny access when check fails return false; }}