π GitGuard β Just-in-Time GitHub Access Control
Submission for the Permit.io Authorization Challenge: Permissions Redefined
What I Built
I built GitGuard - a full-stack, production-grade access control and auditing system for secure, temporary, and role-based GitHub access management that leverages Permit.io for dynamic authorization.
In traditional GitHub environments, access control follows a static model: either you have access to a repository, or you don't. This creates security risks as teams often grant excessive permissions to ensure work isn't blocked. GitGuard solves this by implementing Just-in-Time access - granting temporary, scoped permissions only when needed, verified through biometric authorization.
Think of GitGuard as a "Just-in-Time IAM layer" tailored for GitHub. Perfect for fast-moving teams that need security without sacrificing agility.
Demo Screenshots
π Login Screen
π Register Screen
π Home Page
π Repository Screen
ποΈ Access Request Manager
β Approval/Reject Filter
βοΈ Approve Request Flow
π₯ Access Request Form
𧬠Biometric Approval (Simulated)
Biometrics cannot be captured in screenshots; simulated via mobile preview.
π Push Notification for Approvals
π Repository Details
π Push Notifications List
π’ Organisation List and Create
π Audit Logs
Key Features
Feature | Description |
---|---|
π Biometric Authentication | Approve access requests using fingerprint/face ID |
β±οΈ Just-in-Time Access | Time-bound repository access with automatic expiration |
π₯ Role-Based Access | Multiple repository roles with different permission sets |
π Audit Logging | Comprehensive activity tracking for compliance |
π Push Notifications | Instant alerts for access requests and approvals |
π Multi-Approver Flow | Quorum requirements for sensitive repositories |
π¨ Emergency Access | Expedited access for critical situations |
π Auto-Expiration | Automatic revocation after defined timeframes |
π¦ Organization Grouping | Manage access across multiple organizations |
Role-Based Capabilities
Feature | Viewer | Contributor | Admin |
---|---|---|---|
View Repository | β | β | β |
Clone Repository | β | β | β |
Push Changes | β | β | β |
Approve Access | β | β | β |
Repository Settings | β | β | β |
Delete Repository | β | β | β |
Create Repository | β | β | β |
View Audit Logs | β | β | β |
Project Repositories
Component | Link |
---|---|
π§ Backend | GitGuard Backend |
π± Mobile App | GitGuard Mobile |
Permissions Redefined with Permit.io
GitGuard implements a true Permissions Redefined model using Permit.io's policy-as-code approach, completely separating the business logic from the authorization layer.
Core Authorization Flow
- User initiates an access request for a repository
- Admin receives notification and authenticates with biometrics
- Backend verifies biometric token and processes approval
- Permit.io is used to check, assign, and enforce permissions
- Time-bound role is assigned to the user
- Access is automatically revoked after expiration
ββββββββββββ ββββββββββββββββ ββββββββββββββ ββββββββββββββββ β Mobile βββββΆβ GitGuard API βββββΆβ permitUtilsβββββΆβ Permit.io β β App ββββββ ββββββ middlewareββββββ Cloud PDP β ββββββββββββ ββββββββββββββββ ββββββββββββββ ββββββββββββββββ β β² β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Policies defined in Permit.io dashboard
Implementation
GitGuard implements authorization with a modular, clean approach through a dedicated permitUtils.ts
layer:
// Backend initialization (src/index.ts) export const permit = new Permit({ token: process.env.PERMIT_API_KEY || "", pdp: process.env.PERMIT_PDP_URL || "http://localhost:7766", });
// Permission check implementation (src/utils/permitUtils.ts) export const checkPermission = async ( userId: string, action: string, resource: string, resourceInstance?: string ) => { let resourceObj: string | { type: string; id: string } = resource; // If resource instance is provided, create resource object if (resourceInstance) { resourceObj = { type: resource, id: resourceInstance, }; } // Try to check permission with Permit.io first try { const permitted = await permit.check(userId, action, resourceObj); if (permitted) return true; } catch (permitError) { console.warn( `Permit check failed for user ${userId} on ${resource}:${resourceInstance} - ${permitError}` ); // Continue to fallback check } // If Permit.io check fails or returns false, fall back to database check if (resource === "repository" && resourceInstance) { const { prisma } = await import("../index"); // Check if user is the repository owner const repo = await prisma.repository.findUnique({ where: { id: resourceInstance }, select: { ownerId: true }, }); if (repo && repo.ownerId === userId) { return true; // Repository owners have all permissions } // Check role assignments const roleAssignment = await prisma.roleAssignment.findFirst({ where: { userId, repositoryId: resourceInstance, }, include: { role: { include: { permissions: true, }, }, }, }); if (roleAssignment) { // Check if the assigned role has the required permission const hasPermission = roleAssignment.role.permissions.some( (permission) => permission.action === action || permission.action === "admin" ); if (hasPermission) { return true; } } } return false; };
// Role assignment (src/utils/permitUtils.ts) export const assignRoleInPermit = async ( userId: string, roleKey: string, resourceType: string, resourceInstanceKey: string ) => { try { // First ensure the user exists in Permit.io try { await syncUserWithPermit(userId); } catch (userError) { console.warn(`Could not sync user with Permit.io: ${userError}`); // Continue anyway - we'll try to assign the role } await permit.api.roleAssignments.assign({ user: userId, role: roleKey, tenant: "default", resource_instance: `${resourceType}:${resourceInstanceKey}`, }); console.log( `Successfully assigned role ${roleKey} to user ${userId} for ${resourceType}:${resourceInstanceKey}` ); return true; } catch (error: any) { // If it's a 409 conflict (role already assigned), treat as success if (error.response && error.response.status === 409) { console.log( `Role ${roleKey} already assigned to user ${userId}, skipping` ); return true; } console.error("Failed to assign role in Permit.io:", error); // Don't throw the error, just log it and continue - this makes the app more resilient // We'll fall back to database checks for permissions return false; } };
// Usage in API endpoints (src/routes/repository.ts) router.get("/:id", authenticateJWT, async (req, res, next) => { try { const { id } = req.params; const userId = req.user.id; // Check permission with Permit.io const hasViewPermission = await checkPermission( userId, "view", "repository", id ); if (!hasViewPermission) { throw new ApiError( 403, "You don't have permission to view this repository" ); } // Proceed with repository retrieval... } catch (error) { next(error); } });
Dashboard Configuration
For GitGuard to work correctly, you must configure the following in the Permit.io dashboard:
- Define Resources:
- Create
repository
resource type with actions:-
view
: View repository contents -
clone
: Clone repository -
push
: Push changes to repository -
admin
: Administer repository settings -
delete
: Delete repository -
create
: Create new repository
-
- Define Roles:
-
viewer
: Can view and clone repositories -
contributor
: Can view, clone, and push to repositories -
admin
: Has full access to all repository actions
Configure User-to-Role assignments in the Roles tab
-
Set up Resource Relations for ownership model:
- Relation:
owner
betweenuser
andrepository
- Relation:
Setup Guide
Step 1: Clone the repository
git clone https://github.com/nikhilsahni7/GitGuard.git cd GitGuard
Step 2: Set up Permit.io
- Create a free account at Permit.io
- Create a new project
- Set up:
- Resource type: repository
- Actions: view, clone, push, admin, delete, create
- Roles: viewer, contributor, admin
- Configure role permissions as described above
- Generate an Environment API key from the dashboard
Step 3: Configure environment variables
Create a .env
file in the backend directory:
# Permit.io PERMIT_API_KEY=your_permit_api_key PERMIT_PDP_URL=http://localhost:7766 # Or cloud PDP URL # Database DATABASE_URL=postgresql://user:password@localhost:5432/gitguard # JWT Authentication JWT_SECRET=your_jwt_secret JWT_EXPIRES_IN=7d # GitHub Integration GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret # Push Notifications EXPO_ACCESS_TOKEN=your_expo_token
Step 4: Install dependencies and run
# Backend setup cd backend bun install bun run db:migrate bun run permit:setup bun run dev # Mobile setup (in a separate terminal) cd ../mobile yarn install yarn start
Step 5: Initialize Permit.io
GitGuard includes a setup script that configures all necessary resources and permissions:
cd backend bun run permit:setup
This sets up:
- Repository resource with all actions
- User resource
- Standard roles (viewer, contributor, admin)
- Resource relations for ownership model
Step 6: Verify Permit.io Setup
To verify your Permit.io configuration:
bun run permit:verify
This will:
- Check if resources and roles exist
- Create a test user
- Assign roles and test permissions
- Create and test resource relationships
Challenges and Solutions
Challenge 1: Resource Instance Permissions
Initially, I struggled with implementing resource-instance level permissions in Permit.io to grant access to specific repositories rather than all repositories.
Solution: I implemented a robust resource relations system using Permit.io's API:
// Setup owner relation (setup-permit.ts) const relationData = { key: "owner", name: "Owner", subject_resource: "user", }; await permit.api.resourceRelations.create("repository", relationData); // Create relationship tuples for specific repositories await permit.api.relationshipTuples.create({ subject: `user:${userId}`, relation: "owner", object: `repository:${repositoryId}`, tenant: "default", });
Challenge 2: Fallback Mechanism
What if Permit.io is temporarily unavailable? GitGuard needed resilience.
Solution: I implemented a dual-check system that falls back to database checks:
export const checkPermission = async ( userId, action, resource, resourceInstance ) => { // Try Permit.io first try { const permitted = await permit.check(userId, action, resourceObj); if (permitted) return true; } catch (permitError) { // Log and continue to fallback } // Fallback to database check // [Database permission check logic omitted for brevity] };
Challenge 3: Time-bound Access
Implementing automatic role expiration was critical for the Just-in-Time model.
Solution: Combined Permit.io role assignments with a database TTL mechanism:
// When approving access requests (routes/accessRequest.ts) await prisma.roleAssignment.create({ data: { userId: requestData.userId, repositoryId: requestData.repositoryId, roleId: requestData.roleId, expiresAt: new Date(Date.now() + duration), // Time-bound access approvedBy: adminId, approvedAt: new Date(), }, }); // Also register in Permit.io await assignRoleInPermit( requestData.userId, role.key, "repository", requestData.repositoryId ); // Background job runs to revoke expired access // [Scheduled job implementation omitted for brevity]
Challenge 4: Biometric Verification Flow
Securing the approval process with biometrics while maintaining a smooth user experience was challenging.
Solution: Implemented a secure token-based verification system:
// Mobile app generates a biometric token (mobile code) const biometricAuth = async () => { const compatible = await LocalAuthentication.hasHardwareAsync(); if (!compatible) { throw new Error("Biometric authentication not available"); } const result = await LocalAuthentication.authenticateAsync({ promptMessage: "Authenticate to approve access request", fallbackLabel: "Use passcode", }); if (result.success) { // Generate token only after successful biometric auth return generateBiometricToken(); } throw new Error("Authentication failed"); }; // Backend verifies token before approving (routes/accessRequest.ts) router.post("/:id/approve", authenticateJWT, async (req, res, next) => { try { const { biometricToken } = req.body; const adminId = req.user.id; // Verify biometric token const validToken = await verifyBiometricToken(adminId, biometricToken); if (!validToken) { throw new ApiError(401, "Invalid biometric verification"); } // Process approval with Permit.io // [Approval logic omitted for brevity] } catch (error) { next(error); } });
What I Learned
Building GitGuard with Permit.io provided several key insights:
Technical Benefits
- Separation of Concerns: Clean separation between business logic and authorization decisions
- Flexible Policy Management: Ability to update access policies without code changes
- Resource-Based Model: Modeling GitHub repositories as protected resources with granular permissions
Business Benefits
- Enhanced Security: Just-in-Time access model vastly reduces the attack surface
- Centralized Control: Administrators can manage all permissions from one dashboard
- Audit Compliance: Comprehensive logging of all access decisions
- Reduced Overhead: Automating approval workflows saves significant administrative time
Developer Experience
- Cleaner Codebase: Authorization logic centralized in one place rather than scattered throughout
- Reduced Boilerplate: Fewer permission checks needed in business logic
- Easier Testing: Simpler mocking of authorization decisions for unit tests
Why Permit.io Works for Just-in-Time Access
Permit.io is particularly well-suited for Just-in-Time access control because:
- External Policy Management: Policies can be updated in real-time without deploying code
- Resource Instance Granularity: Permissions can be scoped to specific repositories
- Relationship Modeling: Owner/member relationships easily modeled in permissions
- Flexible Role System: Easy to create and assign temporary roles for specific durations
- Audit Trail: Built-in logging for compliance and security reviews
Future Improvements
With more time, I would enhance GitGuard with:
- Local PDP: Set up a local Policy Decision Point for improved performance and reliability
- Attribute-Based Policies: Extend beyond role-based to include context like time of day, IP range, etc.
- Multi-Tenant Support: Enhanced organization isolation for enterprise environments
- Custom Policy Editor: Allow admins to create custom policies beyond predefined roles
- Integration with CI/CD: Automated access for deployment pipelines with temporary credentials
Built with β€οΈ using:
Bun
β’ Prisma
β’ PostgreSQL
β’ Expo
β’ React Native
β’ TypeScript
β’ Permit.io
Top comments (1)
Good work π