summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/gitlab_cedar_design.md321
-rw-r--r--etc/authzd/gitlab.com/gitlab-org/gitlab/entities.json190
-rw-r--r--etc/authzd/gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd/entities.json138
-rw-r--r--etc/authzd/gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/entities.json138
-rw-r--r--etc/authzd/gitlab_access.cedar127
-rw-r--r--etc/authzd/gitlab_schema.cedarschema158
-rw-r--r--etc/authzd/gitlab_simple.cedar144
-rw-r--r--etc/authzd/gitlab_visibility.cedar127
-rw-r--r--etc/authzd/test_simple.cedar6
-rw-r--r--share/misc/entities_example.json58
-rw-r--r--src/authorization/entities.rs21
-rw-r--r--src/authorization/gitlab_entities.rs208
-rw-r--r--src/gitlab/mod.rs2
-rw-r--r--src/gitlab/project.rs3
-rw-r--r--src/gitlab/user.rs13
15 files changed, 1553 insertions, 101 deletions
diff --git a/docs/gitlab_cedar_design.md b/docs/gitlab_cedar_design.md
new file mode 100644
index 00000000..353b7e53
--- /dev/null
+++ b/docs/gitlab_cedar_design.md
@@ -0,0 +1,321 @@
+# GitLab Cedar Authorization Design (Simplified)
+
+This document describes the simplified design for implementing GitLab-style authorization using Cedar policies in the `authzd` service, without feature or license checks.
+
+## Overview
+
+The `authzd` service implements a Policy Decision Point (PDP) that authorizes GitLab REST API operations using Cedar policies based purely on user access levels and project visibility.
+
+## Architecture
+
+### Entity Model
+
+The Cedar entity model mirrors GitLab's core entities:
+
+- **User**: GitLab users with access levels, admin status, and group memberships
+- **Project**: GitLab projects with visibility settings, feature flags, and member lists
+- **Namespace/Group**: GitLab groups/namespaces with hierarchical relationships
+- **ProjectMembership**: Explicit user-project relationships with access levels
+- **GroupMembership**: User-group relationships with inheritance
+- **Issue/MergeRequest**: Project resources with ownership and state
+
+### Access Levels
+
+GitLab's hierarchical access system is implemented using numeric levels:
+
+- **Guest** (10): Read-only access to allowed resources
+- **Reporter** (20): Can create issues, view CI/CD results
+- **Developer** (30): Can push code, create merge requests
+- **Maintainer** (40): Can manage project settings and members
+- **Owner** (50): Full project control including deletion
+
+### Policy Structure
+
+Cedar policies are organized into separate files by concern:
+
+#### 1. Access Level Policies (`gitlab_simple.cedar`)
+- Implements role-based authorization using GitLab's access levels
+- Grants permissions based on user access level to projects/groups
+- Includes admin override capabilities
+- Blocks access for blocked users
+- No feature or license checks
+
+#### 2. Visibility Policies (`gitlab_visibility.cedar`)
+- Implements GitLab's public/internal/private visibility model
+- Allows public access to public projects
+- Restricts internal projects to non-external users
+- Limits private projects to members only
+- Handles archived project restrictions
+
+## REST API Integration
+
+### Request Mapping
+
+REST API requests are mapped to Cedar authorization checks:
+
+```
+HTTP Request Cedar Authorization
+GET /projects/1 -> can?(User, "read_project", Project::1)
+POST /projects/1/issues -> can?(User, "create_issue", Project::1)
+PUT /projects/1/settings -> can?(User, "admin_project", Project::1)
+DELETE /projects/1 -> can?(User, "destroy_project", Project::1)
+```
+
+### Context Variables
+
+HTTP request context is passed to Cedar policies:
+
+- `method`: HTTP method (GET, POST, PUT, DELETE)
+- `path`: Request path
+- `host`: Request hostname
+- `user_agent`: Client user agent
+- `ip_address`: Client IP address
+
+### Authorization Flow
+
+1. **Extract Principal**: Identify user from JWT token or API key
+2. **Determine Resource**: Extract project/group ID from URL path
+3. **Map Action**: Convert HTTP method + path to Cedar action
+4. **Build Request**: Create Cedar authorization request
+5. **Evaluate**: Execute policies against request
+6. **Decision**: Return allow/deny based on policy evaluation
+
+## Entity Generation
+
+### GitLab API Integration
+
+The CLI tool generates Cedar entities by fetching data from GitLab's REST API:
+
+```rust
+// Generate entities for a project
+let entities = EntitiesRepository::new(api)
+ .all("gitlab-org/gitlab")
+ .await?;
+```
+
+### Entity Structure
+
+Entities are generated with simplified attributes for policy evaluation:
+
+```json
+{
+ "uid": {"type": "Project", "id": "123"},
+ "attrs": {
+ "name": "GitLab",
+ "path": "gitlab",
+ "full_path": "gitlab-org/gitlab",
+ "visibility": "public",
+ "archived": false,
+ "members": ["User::\"1\"", "User::\"2\"", "User::\"3\""]
+ },
+ "parents": [{"type": "Group", "id": "456"}]
+}
+```
+
+User entities include access level and status flags:
+
+```json
+{
+ "uid": {"type": "User", "id": "1"},
+ "attrs": {
+ "username": "alice",
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
+ },
+ "parents": []
+}
+```
+
+## Implementation Components
+
+### 1. Enhanced GitLab Types
+
+Updated GitLab API types include all fields needed for authorization:
+
+- **Project**: visibility, features, access levels, archived status
+- **User**: admin, blocked, external, bot flags
+- **Group**: visibility, hierarchy, member access levels
+
+### 2. Cedar Entity Builders
+
+The `GitLabEntityBuilder` converts GitLab API responses to Cedar entities:
+
+```rust
+impl GitLabEntityBuilder {
+ pub fn build_project(project: &Project) -> CedarEntity { ... }
+ pub fn build_user(user: &User) -> CedarEntity { ... }
+ pub fn build_membership(user_id: u64, project_id: u64, member: &Member) -> CedarEntity { ... }
+}
+```
+
+### 3. Authorization Service
+
+The `CedarAuthorizer` evaluates requests against policies:
+
+```rust
+pub fn authorize(&self, principal: &str, action: &str, resource: &str, context: &Context) -> Result<Decision> {
+ let request = Request::new(principal, action, resource, context);
+ self.authorizer.is_authorized(&request, &self.policies, &self.entities)
+}
+```
+
+## gRPC Service Design
+
+### Authorization Service
+
+Implement gRPC endpoints that mirror GitLab's `Ability.allowed?` method:
+
+```protobuf
+service GitLabAuthorization {
+ rpc IsAllowed(IsAllowedRequest) returns (IsAllowedResponse);
+ rpc BatchIsAllowed(BatchIsAllowedRequest) returns (BatchIsAllowedResponse);
+}
+
+message IsAllowedRequest {
+ string user_id = 1;
+ string action = 2;
+ string resource_type = 3;
+ string resource_id = 4;
+ map<string, string> context = 5;
+}
+
+message IsAllowedResponse {
+ bool allowed = 1;
+ string reason = 2;
+}
+```
+
+### Performance Optimizations
+
+- **Entity Caching**: Cache entities in memory with TTL
+- **Policy Compilation**: Pre-compile policies for faster evaluation
+- **Batch Operations**: Support bulk authorization checks
+- **Connection Pooling**: Pool GitLab API connections
+
+## Deployment Patterns
+
+### Standalone PDP
+
+Deploy `authzd` as a standalone authorization service:
+
+- Applications make gRPC calls for authorization decisions
+- Entities are refreshed periodically from GitLab API
+- Policies are updated via configuration management
+
+### Envoy Integration
+
+Use as Envoy external authorization service:
+
+- Intercept HTTP requests at API gateway
+- Make real-time authorization decisions
+- Block unauthorized requests before reaching application
+
+### Library Integration
+
+Embed authorization logic in applications:
+
+- Link `authzd` as a Rust library
+- Perform in-process authorization checks
+- Reduce network latency for high-frequency operations
+
+## Configuration
+
+### Environment Variables
+
+```bash
+GITLAB_TOKEN=glpat-xxx # GitLab API token
+GITLAB_HOST=gitlab.example.com # GitLab instance URL
+AUTHZD_PORT=50051 # gRPC service port
+ENTITY_REFRESH_INTERVAL=300 # Seconds between entity updates
+POLICY_PATH=/etc/authzd # Path to Cedar policy files
+```
+
+### Policy Management
+
+- Store policies in version control
+- Use GitOps for policy deployment
+- Implement policy validation in CI/CD
+- Support hot-reloading of policy changes
+
+## Testing Strategy
+
+### Unit Tests
+
+- Test individual policy rules
+- Verify entity generation accuracy
+- Validate API type parsing
+
+### Integration Tests
+
+- Test complete authorization flows
+- Verify GitLab API integration
+- Test gRPC service endpoints
+
+### Policy Tests
+
+- Use Cedar's policy testing framework
+- Create test scenarios for common GitLab operations
+- Validate policy correctness with real GitLab data
+
+## Security Considerations
+
+### Token Management
+
+- Rotate GitLab API tokens regularly
+- Use least-privilege tokens for entity generation
+- Secure token storage in production
+
+### Entity Data
+
+- Sanitize sensitive data in entities
+- Implement data retention policies
+- Audit entity access and modifications
+
+### Policy Security
+
+- Validate policies before deployment
+- Implement policy signing/verification
+- Monitor for policy conflicts or errors
+
+## Migration Strategy
+
+### Phase 1: Read-Only Operations
+
+- Implement authorization for GET requests
+- Test with non-critical projects
+- Validate performance and accuracy
+
+### Phase 2: Write Operations
+
+- Add support for POST/PUT/DELETE operations
+- Implement fine-grained permissions
+- Roll out to production gradually
+
+### Phase 3: Advanced Features
+
+- Add support for custom roles
+- Implement audit logging
+- Add policy analytics and monitoring
+
+## Monitoring and Observability
+
+### Metrics
+
+- Authorization request rate and latency
+- Policy evaluation time
+- Entity refresh success/failure rates
+- Cache hit/miss ratios
+
+### Logging
+
+- Log all authorization decisions
+- Include request context and policy evaluation details
+- Implement structured logging for analysis
+
+### Alerting
+
+- Alert on authorization failures
+- Monitor entity refresh errors
+- Track policy evaluation performance \ No newline at end of file
diff --git a/etc/authzd/gitlab.com/gitlab-org/gitlab/entities.json b/etc/authzd/gitlab.com/gitlab-org/gitlab/entities.json
index 1992a9c7..f0e61bf3 100644
--- a/etc/authzd/gitlab.com/gitlab-org/gitlab/entities.json
+++ b/etc/authzd/gitlab.com/gitlab-org/gitlab/entities.json
@@ -7,7 +7,31 @@
"attrs": {
"name": "GitLab",
"path": "gitlab",
- "full_path": "gitlab-org/gitlab"
+ "full_path": "gitlab-org/gitlab",
+ "visibility": "public",
+ "archived": false,
+ "members": [
+ "User::\"1\"",
+ "User::\"263716\"",
+ "User::\"2293\"",
+ "User::\"283999\"",
+ "User::\"370493\"",
+ "User::\"138401\"",
+ "User::\"516904\"",
+ "User::\"527558\"",
+ "User::\"215818\"",
+ "User::\"429540\"",
+ "User::\"581582\"",
+ "User::\"626804\"",
+ "User::\"597578\"",
+ "User::\"739252\"",
+ "User::\"201566\"",
+ "User::\"829774\"",
+ "User::\"4849\"",
+ "User::\"790854\"",
+ "User::\"273486\"",
+ "User::\"411701\""
+ ]
},
"parents": [
{
@@ -23,7 +47,10 @@
},
"attrs": {
"username": "sytses",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -34,7 +61,10 @@
},
"attrs": {
"username": "grzesiek",
- "access_level": 40
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -45,7 +75,38 @@
},
"attrs": {
"username": "brodock",
- "access_level": 40
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
+ },
+ "parents": []
+ },
+ {
+ "uid": {
+ "type": "User",
+ "id": "283999"
+ },
+ "attrs": {
+ "username": "dbalexandre",
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
+ },
+ "parents": []
+ },
+ {
+ "uid": {
+ "type": "User",
+ "id": "370493"
+ },
+ "attrs": {
+ "username": "luke",
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -56,7 +117,24 @@
},
"attrs": {
"username": "chriscool",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
+ },
+ "parents": []
+ },
+ {
+ "uid": {
+ "type": "User",
+ "id": "516904"
+ },
+ "attrs": {
+ "username": "tauriedavis",
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -67,7 +145,10 @@
},
"attrs": {
"username": "eliran.mesika",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -78,7 +159,10 @@
},
"attrs": {
"username": "tmaczukin",
- "access_level": 40
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -89,7 +173,10 @@
},
"attrs": {
"username": "ahanselka",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -100,7 +187,10 @@
},
"attrs": {
"username": "arihantar",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -111,7 +201,10 @@
},
"attrs": {
"username": "pedroms",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -122,7 +215,10 @@
},
"attrs": {
"username": "WarheadsSE",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -133,18 +229,10 @@
},
"attrs": {
"username": "jdrumtra",
- "access_level": 30
- },
- "parents": []
- },
- {
- "uid": {
- "type": "User",
- "id": "739361"
- },
- "attrs": {
- "username": "Elsje",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -155,7 +243,10 @@
},
"attrs": {
"username": "annabeldunstone",
- "access_level": 40
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -166,7 +257,10 @@
},
"attrs": {
"username": "jivanvl",
- "access_level": 40
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -177,7 +271,10 @@
},
"attrs": {
"username": "balasankarc",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -188,7 +285,10 @@
},
"attrs": {
"username": "harishsr",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -199,7 +299,10 @@
},
"attrs": {
"username": "jameslopez",
- "access_level": 40
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -210,29 +313,10 @@
},
"attrs": {
"username": "kushalpandya",
- "access_level": 40
- },
- "parents": []
- },
- {
- "uid": {
- "type": "User",
- "id": "508743"
- },
- "attrs": {
- "username": "jarka",
- "access_level": 40
- },
- "parents": []
- },
- {
- "uid": {
- "type": "User",
- "id": "506061"
- },
- "attrs": {
- "username": "ahmadsherif",
- "access_level": 30
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -244,7 +328,9 @@
"attrs": {
"name": "GitLab.org",
"path": "gitlab-org",
- "full_path": "gitlab-org"
+ "full_path": "gitlab-org",
+ "visibility": "private",
+ "members": []
},
"parents": []
}
diff --git a/etc/authzd/gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd/entities.json b/etc/authzd/gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd/entities.json
index 6bc513fb..6416ec72 100644
--- a/etc/authzd/gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd/entities.json
+++ b/etc/authzd/gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd/entities.json
@@ -7,7 +7,31 @@
"attrs": {
"name": "authz.d",
"path": "authzd",
- "full_path": "gitlab-org/software-supply-chain-security/authorization/authzd"
+ "full_path": "gitlab-org/software-supply-chain-security/authorization/authzd",
+ "visibility": "private",
+ "archived": false,
+ "members": [
+ "User::\"1\"",
+ "User::\"116\"",
+ "User::\"13356\"",
+ "User::\"3585\"",
+ "User::\"12452\"",
+ "User::\"64248\"",
+ "User::\"263716\"",
+ "User::\"283999\"",
+ "User::\"2293\"",
+ "User::\"215818\"",
+ "User::\"128633\"",
+ "User::\"273486\"",
+ "User::\"201566\"",
+ "User::\"426128\"",
+ "User::\"138401\"",
+ "User::\"367626\"",
+ "User::\"516904\"",
+ "User::\"527558\"",
+ "User::\"429540\"",
+ "User::\"506061\""
+ ]
},
"parents": [
{
@@ -23,7 +47,10 @@
},
"attrs": {
"username": "sytses",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -34,7 +61,10 @@
},
"attrs": {
"username": "marin",
- "access_level": 50
+ "access_level": 50,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -45,7 +75,10 @@
},
"attrs": {
"username": "dblessing",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -56,7 +89,10 @@
},
"attrs": {
"username": "axil",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -67,7 +103,10 @@
},
"attrs": {
"username": "ayufan",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -78,7 +117,10 @@
},
"attrs": {
"username": "stanhu",
- "access_level": 50
+ "access_level": 50,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -89,7 +131,10 @@
},
"attrs": {
"username": "grzesiek",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -100,7 +145,10 @@
},
"attrs": {
"username": "dbalexandre",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -111,7 +159,10 @@
},
"attrs": {
"username": "brodock",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -122,7 +173,10 @@
},
"attrs": {
"username": "tmaczukin",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -133,7 +187,10 @@
},
"attrs": {
"username": "rymai",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -144,7 +201,10 @@
},
"attrs": {
"username": "jameslopez",
- "access_level": 40
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -155,7 +215,10 @@
},
"attrs": {
"username": "annabeldunstone",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -166,7 +229,10 @@
},
"attrs": {
"username": "felipe_artur",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -177,7 +243,10 @@
},
"attrs": {
"username": "chriscool",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -188,7 +257,10 @@
},
"attrs": {
"username": "alejandro",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -199,7 +271,10 @@
},
"attrs": {
"username": "tauriedavis",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -210,7 +285,10 @@
},
"attrs": {
"username": "eliran.mesika",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -221,7 +299,10 @@
},
"attrs": {
"username": "ahanselka",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -232,7 +313,10 @@
},
"attrs": {
"username": "ahmadsherif",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -244,7 +328,9 @@
"attrs": {
"name": "GitLab.org",
"path": "gitlab-org",
- "full_path": "gitlab-org"
+ "full_path": "gitlab-org",
+ "visibility": "private",
+ "members": []
},
"parents": []
},
@@ -256,7 +342,9 @@
"attrs": {
"name": "software-supply-chain-security",
"path": "software-supply-chain-security",
- "full_path": "gitlab-org/software-supply-chain-security"
+ "full_path": "gitlab-org/software-supply-chain-security",
+ "visibility": "private",
+ "members": []
},
"parents": [
{
@@ -273,7 +361,9 @@
"attrs": {
"name": "Authorization",
"path": "authorization",
- "full_path": "gitlab-org/software-supply-chain-security/authorization"
+ "full_path": "gitlab-org/software-supply-chain-security/authorization",
+ "visibility": "private",
+ "members": []
},
"parents": [
{
diff --git a/etc/authzd/gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/entities.json b/etc/authzd/gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/entities.json
index 4846592a..28c07b12 100644
--- a/etc/authzd/gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/entities.json
+++ b/etc/authzd/gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/entities.json
@@ -7,7 +7,31 @@
"attrs": {
"name": "sparkle.d",
"path": "sparkled",
- "full_path": "gitlab-org/software-supply-chain-security/authorization/sparkled"
+ "full_path": "gitlab-org/software-supply-chain-security/authorization/sparkled",
+ "visibility": "private",
+ "archived": false,
+ "members": [
+ "User::\"1\"",
+ "User::\"116\"",
+ "User::\"13356\"",
+ "User::\"3585\"",
+ "User::\"12452\"",
+ "User::\"64248\"",
+ "User::\"263716\"",
+ "User::\"283999\"",
+ "User::\"2293\"",
+ "User::\"215818\"",
+ "User::\"128633\"",
+ "User::\"273486\"",
+ "User::\"201566\"",
+ "User::\"426128\"",
+ "User::\"138401\"",
+ "User::\"367626\"",
+ "User::\"516904\"",
+ "User::\"527558\"",
+ "User::\"429540\"",
+ "User::\"506061\""
+ ]
},
"parents": [
{
@@ -23,7 +47,10 @@
},
"attrs": {
"username": "sytses",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -34,7 +61,10 @@
},
"attrs": {
"username": "marin",
- "access_level": 50
+ "access_level": 50,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -45,7 +75,10 @@
},
"attrs": {
"username": "dblessing",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -56,7 +89,10 @@
},
"attrs": {
"username": "axil",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -67,7 +103,10 @@
},
"attrs": {
"username": "ayufan",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -78,7 +117,10 @@
},
"attrs": {
"username": "stanhu",
- "access_level": 50
+ "access_level": 50,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -89,7 +131,10 @@
},
"attrs": {
"username": "grzesiek",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -100,7 +145,10 @@
},
"attrs": {
"username": "dbalexandre",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -111,7 +159,10 @@
},
"attrs": {
"username": "brodock",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -122,7 +173,10 @@
},
"attrs": {
"username": "tmaczukin",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -133,7 +187,10 @@
},
"attrs": {
"username": "rymai",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -144,7 +201,10 @@
},
"attrs": {
"username": "jameslopez",
- "access_level": 40
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -155,7 +215,10 @@
},
"attrs": {
"username": "annabeldunstone",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -166,7 +229,10 @@
},
"attrs": {
"username": "felipe_artur",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -177,7 +243,10 @@
},
"attrs": {
"username": "chriscool",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -188,7 +257,10 @@
},
"attrs": {
"username": "alejandro",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -199,7 +271,10 @@
},
"attrs": {
"username": "tauriedavis",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -210,7 +285,10 @@
},
"attrs": {
"username": "eliran.mesika",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -221,7 +299,10 @@
},
"attrs": {
"username": "ahanselka",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -232,7 +313,10 @@
},
"attrs": {
"username": "ahmadsherif",
- "access_level": 30
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
},
"parents": []
},
@@ -244,7 +328,9 @@
"attrs": {
"name": "GitLab.org",
"path": "gitlab-org",
- "full_path": "gitlab-org"
+ "full_path": "gitlab-org",
+ "visibility": "private",
+ "members": []
},
"parents": []
},
@@ -256,7 +342,9 @@
"attrs": {
"name": "software-supply-chain-security",
"path": "software-supply-chain-security",
- "full_path": "gitlab-org/software-supply-chain-security"
+ "full_path": "gitlab-org/software-supply-chain-security",
+ "visibility": "private",
+ "members": []
},
"parents": [
{
@@ -273,7 +361,9 @@
"attrs": {
"name": "Authorization",
"path": "authorization",
- "full_path": "gitlab-org/software-supply-chain-security/authorization"
+ "full_path": "gitlab-org/software-supply-chain-security/authorization",
+ "visibility": "private",
+ "members": []
},
"parents": [
{
diff --git a/etc/authzd/gitlab_access.cedar b/etc/authzd/gitlab_access.cedar
new file mode 100644
index 00000000..ca17aa67
--- /dev/null
+++ b/etc/authzd/gitlab_access.cedar
@@ -0,0 +1,127 @@
+// GitLab Access Level Based Authorization
+// Maps to Gitlab::Access constants: Guest(10), Reporter(20), Developer(30), Maintainer(40), Owner(50)
+// Guest access (read-only operations)
+permit (
+ principal is User,
+ action in
+ [Action::"read_project",
+ Action::"read_group",
+ Action::"read_issue",
+ Action::"read_merge_request",
+ Action::"read_pipeline",
+ Action::"read_wiki",
+ Action::"download_code"],
+ resource
+)
+when
+{
+ principal has access_level &&
+ principal.access_level >= 10 &&
+ resource has visibility &&
+ (resource.visibility == "public" ||
+ resource has members &&
+ principal in resource.members)
+};
+
+// Reporter access (can create issues, view builds)
+permit (
+ principal is User,
+ action in
+ [Action::"create_issue",
+ Action::"create_issue_note",
+ Action::"read_build",
+ Action::"read_container_image",
+ Action::"pull_container_image"],
+ resource
+)
+when
+{
+ principal has access_level &&
+ principal.access_level >= 20 &&
+ resource has members &&
+ principal in resource.members
+};
+
+// Developer access (can push code, create MRs)
+permit (
+ principal is User,
+ action in
+ [Action::"push_code",
+ Action::"create_merge_request",
+ Action::"update_merge_request",
+ Action::"create_pipeline",
+ Action::"retry_pipeline",
+ Action::"push_container_image",
+ Action::"create_release"],
+ resource
+)
+when
+{
+ principal has access_level &&
+ principal.access_level >= 30 &&
+ resource has members &&
+ principal in resource.members
+};
+
+// Maintainer access (project administration)
+permit (
+ principal is User,
+ action in
+ [Action::"admin_project",
+ Action::"manage_project_members",
+ Action::"admin_merge_request",
+ Action::"push_to_delete_protected_branch",
+ Action::"admin_pipeline",
+ Action::"admin_container_registry",
+ Action::"admin_package_registry"],
+ resource
+)
+when
+{
+ principal has access_level &&
+ principal.access_level >= 40 &&
+ resource has members &&
+ principal in resource.members
+};
+
+// Owner access (full project control)
+permit (
+ principal is User,
+ action in
+ [Action::"destroy_project",
+ Action::"transfer_project",
+ Action::"archive_project",
+ Action::"change_visibility_level",
+ Action::"admin_project_hooks",
+ Action::"admin_project_runners"],
+ resource
+)
+when
+{
+ principal has access_level &&
+ principal.access_level >= 50 &&
+ resource has members &&
+ principal in resource.members
+};
+
+// Admin override - can do everything
+permit (
+ principal is User,
+ action,
+ resource
+)
+when
+{
+ principal has admin &&
+ principal.admin == true &&
+ principal has blocked &&
+ !principal.blocked
+};
+
+// Block all access for blocked users
+forbid (
+ principal is User,
+ action,
+ resource
+)
+when { principal has blocked && principal.blocked == true };
diff --git a/etc/authzd/gitlab_schema.cedarschema b/etc/authzd/gitlab_schema.cedarschema
new file mode 100644
index 00000000..78d7bd1a
--- /dev/null
+++ b/etc/authzd/gitlab_schema.cedarschema
@@ -0,0 +1,158 @@
+// GitLab Cedar Schema Definition
+// Defines entity types and actions for GitLab authorization
+
+// User entity represents GitLab users
+entity User = {
+ username: String,
+ name: String,
+ admin: Bool,
+ blocked: Bool,
+ external: Bool,
+ bot: Bool,
+ access_level: Long,
+};
+
+// Group/Namespace entity (can be nested)
+entity Namespace = {
+ name: String,
+ path: String,
+ full_path: String,
+ kind: String, // "user" or "group"
+ visibility_level: String,
+ members: Set<User>,
+} tags Set<String>;
+
+// Project entity represents GitLab projects
+entity Project = {
+ name: String,
+ path: String,
+ full_path: String,
+ visibility: String, // "public", "internal", "private"
+ archived: Bool,
+ members: Set<User>, // Project members
+} tags Set<String>;
+
+// Group alias for Namespace
+entity Group = {
+ name: String,
+ path: String,
+ full_path: String,
+ visibility: String,
+ members: Set<User>,
+} tags Set<String>;
+
+// Project membership relationship
+entity ProjectMembership = {
+ user_id: Long,
+ project_id: Long,
+ access_level: Long,
+ expires_at: String,
+} tags Set<String>;
+
+// Group membership relationship
+entity GroupMembership = {
+ user_id: Long,
+ group_id: Long,
+ access_level: Long,
+ expires_at: String,
+} tags Set<String>;
+
+// Issue entity
+entity Issue = {
+ iid: Long,
+ title: String,
+ state: String,
+ confidential: Bool,
+ author_id: Long,
+ assignee_ids: Set<Long>,
+ created_at: String,
+ updated_at: String,
+} tags Set<String>;
+
+// Merge Request entity
+entity MergeRequest = {
+ iid: Long,
+ title: String,
+ state: String,
+ merge_status: String,
+ author_id: Long,
+ assignee_id: Long,
+ target_branch: String,
+ source_branch: String,
+ work_in_progress: Bool,
+ created_at: String,
+ updated_at: String,
+} tags Set<String>;
+
+// Actions that can be performed
+action "read_project";
+action "admin_project";
+action "destroy_project";
+action "transfer_project";
+action "archive_project";
+action "change_visibility_level";
+action "manage_project_members";
+
+action "read_group";
+action "admin_group";
+action "read_group_details";
+
+action "read_repository";
+action "download_code";
+action "push_code";
+action "admin_repository";
+action "push_to_delete_protected_branch";
+
+action "read_issue";
+action "create_issue";
+action "update_issue";
+action "admin_issue";
+action "create_issue_note";
+
+action "read_merge_request";
+action "create_merge_request";
+action "update_merge_request";
+action "admin_merge_request";
+action "merge_merge_request";
+
+action "read_wiki";
+action "create_wiki_page";
+action "update_wiki_page";
+action "admin_wiki";
+
+action "read_snippet";
+action "create_snippet";
+action "update_snippet";
+action "admin_snippet";
+
+action "read_build";
+action "read_pipeline";
+action "create_pipeline";
+action "retry_pipeline";
+action "admin_pipeline";
+
+action "read_container_image";
+action "pull_container_image";
+action "push_container_image";
+action "admin_container_registry";
+
+action "read_package";
+action "pull_package";
+action "push_package";
+action "admin_package_registry";
+
+action "read_analytics";
+action "read_cycle_analytics";
+action "read_repository_analytics";
+
+action "read_security_report";
+action "admin_security_policy";
+action "read_vulnerability_report";
+
+action "read_release";
+action "create_release";
+action "update_release";
+action "admin_release";
+
+action "admin_project_hooks";
+action "admin_project_runners"; \ No newline at end of file
diff --git a/etc/authzd/gitlab_simple.cedar b/etc/authzd/gitlab_simple.cedar
new file mode 100644
index 00000000..5ea8757d
--- /dev/null
+++ b/etc/authzd/gitlab_simple.cedar
@@ -0,0 +1,144 @@
+// Simplified GitLab Authorization - No Feature or License Checks
+// Based purely on user access levels and project visibility
+// Guest access (read-only operations)
+permit (
+ principal is User,
+ action in
+ [Action::"read_project",
+ Action::"read_group",
+ Action::"read_issue",
+ Action::"read_merge_request",
+ Action::"read_pipeline",
+ Action::"read_wiki",
+ Action::"read_repository",
+ Action::"download_code",
+ Action::"read_snippet",
+ Action::"read_container_image",
+ Action::"read_package",
+ Action::"read_build",
+ Action::"read_analytics",
+ Action::"read_release"],
+ resource
+)
+when
+{
+ principal has access_level &&
+ principal.access_level >= 10 &&
+ resource has visibility &&
+ (resource.visibility == "public" ||
+ resource has members &&
+ principal in resource.members)
+};
+
+// Reporter access (can create issues, notes)
+permit (
+ principal is User,
+ action in
+ [Action::"create_issue",
+ Action::"create_issue_note",
+ Action::"pull_container_image",
+ Action::"pull_package"],
+ resource
+)
+when
+{
+ principal has access_level &&
+ principal.access_level >= 20 &&
+ resource has members &&
+ principal in resource.members
+};
+
+// Developer access (can push code, create MRs)
+permit (
+ principal is User,
+ action in
+ [Action::"push_code",
+ Action::"create_merge_request",
+ Action::"update_merge_request",
+ Action::"update_issue",
+ Action::"create_pipeline",
+ Action::"retry_pipeline",
+ Action::"push_container_image",
+ Action::"push_package",
+ Action::"create_release",
+ Action::"create_wiki_page",
+ Action::"update_wiki_page",
+ Action::"create_snippet",
+ Action::"update_snippet"],
+ resource
+)
+when
+{
+ principal has access_level &&
+ principal.access_level >= 30 &&
+ resource has members &&
+ principal in resource.members
+};
+
+// Maintainer access (project administration)
+permit (
+ principal is User,
+ action in
+ [Action::"admin_project",
+ Action::"manage_project_members",
+ Action::"admin_merge_request",
+ Action::"push_to_delete_protected_branch",
+ Action::"admin_pipeline",
+ Action::"admin_container_registry",
+ Action::"admin_package_registry",
+ Action::"admin_wiki",
+ Action::"admin_snippet",
+ Action::"admin_repository",
+ Action::"admin_issue",
+ Action::"admin_release"],
+ resource
+)
+when
+{
+ principal has access_level &&
+ principal.access_level >= 40 &&
+ resource has members &&
+ principal in resource.members
+};
+
+// Owner access (full project control)
+permit (
+ principal is User,
+ action in
+ [Action::"destroy_project",
+ Action::"transfer_project",
+ Action::"archive_project",
+ Action::"change_visibility_level",
+ Action::"admin_project_hooks",
+ Action::"admin_project_runners"],
+ resource
+)
+when
+{
+ principal has access_level &&
+ principal.access_level >= 50 &&
+ resource has members &&
+ principal in resource.members
+};
+
+// Admin override - can do everything
+permit (
+ principal is User,
+ action,
+ resource
+)
+when
+{
+ principal has admin &&
+ principal.admin == true &&
+ principal has blocked &&
+ !principal.blocked
+};
+
+// Block all access for blocked users
+forbid (
+ principal is User,
+ action,
+ resource
+)
+when { principal has blocked && principal.blocked == true };
diff --git a/etc/authzd/gitlab_visibility.cedar b/etc/authzd/gitlab_visibility.cedar
new file mode 100644
index 00000000..78049cae
--- /dev/null
+++ b/etc/authzd/gitlab_visibility.cedar
@@ -0,0 +1,127 @@
+// GitLab Visibility Level Authorization
+// Controls access based on project/group visibility: public, internal, private
+// Public projects - anyone can read
+permit (
+ principal,
+ action in
+ [Action::"read_project",
+ Action::"read_repository",
+ Action::"download_code",
+ Action::"read_issue",
+ Action::"read_merge_request",
+ Action::"read_wiki",
+ Action::"read_snippet"],
+ resource
+)
+when
+{
+ resource has visibility &&
+ resource.visibility == "public" &&
+ principal has blocked &&
+ !principal.blocked
+};
+
+// Internal projects - authenticated users can read
+permit (
+ principal is User,
+ action in
+ [Action::"read_project",
+ Action::"read_repository",
+ Action::"download_code",
+ Action::"read_issue",
+ Action::"read_merge_request",
+ Action::"read_wiki",
+ Action::"read_snippet"],
+ resource
+)
+when
+{
+ resource has visibility &&
+ resource.visibility == "internal" &&
+ principal has external &&
+ !principal.external &&
+ principal has blocked &&
+ !principal.blocked
+};
+
+// Private projects - only members can access
+permit (
+ principal is User,
+ action in
+ [Action::"read_project",
+ Action::"read_repository",
+ Action::"download_code",
+ Action::"read_issue",
+ Action::"read_merge_request",
+ Action::"read_wiki",
+ Action::"read_snippet"],
+ resource
+)
+when
+{
+ resource has visibility &&
+ resource.visibility == "private" &&
+ principal in resource.members &&
+ principal has blocked &&
+ !principal.blocked
+};
+
+// Prevent external users from accessing internal projects
+forbid (
+ principal is User,
+ action,
+ resource
+)
+when
+{
+ resource has visibility &&
+ resource.visibility == "internal" &&
+ principal has external &&
+ principal.external == true
+};
+
+// Group visibility rules - similar to projects
+permit (
+ principal,
+ action in [Action::"read_group", Action::"read_group_details"],
+ resource is Group
+)
+when { resource has visibility && resource.visibility == "public" };
+
+permit (
+ principal is User,
+ action in [Action::"read_group", Action::"read_group_details"],
+ resource is Group
+)
+when
+{
+ resource has visibility &&
+ resource.visibility == "internal" &&
+ principal has external &&
+ !principal.external
+};
+
+permit (
+ principal is User,
+ action in [Action::"read_group", Action::"read_group_details"],
+ resource is Group
+)
+when
+{
+ resource has visibility &&
+ resource.visibility == "private" &&
+ principal in resource.members
+};
+
+// Archived projects have limited access
+forbid (
+ principal,
+ action in
+ [Action::"push_code",
+ Action::"create_issue",
+ Action::"create_merge_request",
+ Action::"update_issue",
+ Action::"update_merge_request"],
+ resource
+)
+when { resource has archived && resource.archived == true };
diff --git a/etc/authzd/test_simple.cedar b/etc/authzd/test_simple.cedar
new file mode 100644
index 00000000..d236bc7f
--- /dev/null
+++ b/etc/authzd/test_simple.cedar
@@ -0,0 +1,6 @@
+// Simple test policy to validate basic Cedar syntax
+permit (
+ principal is User,
+ action == Action::"read_project",
+ resource is Project
+);
diff --git a/share/misc/entities_example.json b/share/misc/entities_example.json
new file mode 100644
index 00000000..c93f76f3
--- /dev/null
+++ b/share/misc/entities_example.json
@@ -0,0 +1,58 @@
+[
+ {
+ "uid": {"type": "Project", "id": "123"},
+ "attrs": {
+ "name": "GitLab",
+ "path": "gitlab",
+ "full_path": "gitlab-org/gitlab",
+ "visibility": "public",
+ "archived": false,
+ "members": ["User::\"1\"", "User::\"2\"", "User::\"3\""]
+ },
+ "parents": [{"type": "Group", "id": "456"}]
+ },
+ {
+ "uid": {"type": "Group", "id": "456"},
+ "attrs": {
+ "name": "GitLab.org",
+ "path": "gitlab-org",
+ "full_path": "gitlab-org",
+ "visibility": "private",
+ "members": ["User::\"1\"", "User::\"2\""]
+ },
+ "parents": []
+ },
+ {
+ "uid": {"type": "User", "id": "1"},
+ "attrs": {
+ "username": "alice",
+ "access_level": 40,
+ "admin": false,
+ "blocked": false,
+ "external": false
+ },
+ "parents": []
+ },
+ {
+ "uid": {"type": "User", "id": "2"},
+ "attrs": {
+ "username": "bob",
+ "access_level": 30,
+ "admin": false,
+ "blocked": false,
+ "external": false
+ },
+ "parents": []
+ },
+ {
+ "uid": {"type": "User", "id": "3"},
+ "attrs": {
+ "username": "charlie",
+ "access_level": 20,
+ "admin": false,
+ "blocked": false,
+ "external": false
+ },
+ "parents": []
+ }
+] \ No newline at end of file
diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs
index fc1246d7..8d7d178f 100644
--- a/src/authorization/entities.rs
+++ b/src/authorization/entities.rs
@@ -46,6 +46,16 @@ impl EntitiesRepository {
let project = self.api.get_project(&project_path).await?;
+ // Create member list first
+ let member_ids: Vec<String> = self
+ .api
+ .get_project_members(project.id)
+ .await?
+ .iter()
+ .filter(|m| m.state == "active")
+ .map(|m| format!("User::\"{}\"", m.id))
+ .collect();
+
entities.push(CedarEntity {
uid: CedarUid {
entity_type: "Project".to_string(),
@@ -54,7 +64,10 @@ impl EntitiesRepository {
attrs: serde_json::json!({
"name": project.name,
"path": project.path,
- "full_path": format!("{}/{}", project.namespace.full_path, project.path),
+ "full_path": project.path_with_namespace,
+ "visibility": project.visibility,
+ "archived": project.archived.unwrap_or(false),
+ "members": member_ids,
}),
parents: if project.namespace.kind == "group" {
vec![CedarParent {
@@ -66,6 +79,7 @@ impl EntitiesRepository {
},
});
+ // Get all members again to create User entities
for member in self.api.get_project_members(project.id).await? {
if member.state == "active" {
entities.push(CedarEntity {
@@ -76,6 +90,9 @@ impl EntitiesRepository {
attrs: serde_json::json!({
"username": member.username,
"access_level": member.access_level,
+ "admin": false, // Would need to fetch from user API for real admin status
+ "blocked": false, // Would need to fetch from user API for real blocked status
+ "external": false, // Would need to fetch from user API for real external status
}),
parents: vec![],
});
@@ -133,6 +150,8 @@ impl EntitiesRepository {
"name": group.name,
"path": group.path,
"full_path": group.full_path,
+ "visibility": "private", // Groups don't have visibility in our simplified model
+ "members": [], // Would need group members API to populate
}),
parents,
});
diff --git a/src/authorization/gitlab_entities.rs b/src/authorization/gitlab_entities.rs
new file mode 100644
index 00000000..643f90e8
--- /dev/null
+++ b/src/authorization/gitlab_entities.rs
@@ -0,0 +1,208 @@
+use serde::Serialize;
+use std::collections::HashMap;
+
+// Cedar entity structures optimized for GitLab authorization
+#[derive(Debug, Serialize)]
+pub struct CedarEntity {
+ pub uid: CedarUid,
+ pub attrs: serde_json::Value,
+ pub parents: Vec<CedarParent>,
+}
+
+#[derive(Debug, Serialize)]
+pub struct CedarUid {
+ #[serde(rename = "type")]
+ pub entity_type: String,
+ pub id: String,
+}
+
+#[derive(Debug, Serialize)]
+pub struct CedarParent {
+ #[serde(rename = "type")]
+ pub parent_type: String,
+ pub id: String,
+}
+
+// GitLab-specific entity builders
+pub struct GitLabEntityBuilder;
+
+impl GitLabEntityBuilder {
+ // Build User entity with GitLab-specific attributes
+ pub fn build_user(user: &crate::gitlab::User) -> CedarEntity {
+ CedarEntity {
+ uid: CedarUid {
+ entity_type: "User".to_string(),
+ id: user.id.to_string(),
+ },
+ attrs: serde_json::json!({
+ "username": user.username,
+ "name": user.name,
+ "admin": user.admin.unwrap_or(false),
+ "blocked": user.state == "blocked",
+ "bot": user.bot.unwrap_or(false),
+ "external": user.external.unwrap_or(false),
+ "created_at": user.created_at
+ }),
+ parents: vec![],
+ }
+ }
+
+ // Build Project entity - simplified without feature checks
+ pub fn build_project(project: &crate::gitlab::Project) -> CedarEntity {
+ CedarEntity {
+ uid: CedarUid {
+ entity_type: "Project".to_string(),
+ id: project.id.to_string(),
+ },
+ attrs: serde_json::json!({
+ "name": project.name,
+ "path": project.path,
+ "full_path": project.path_with_namespace,
+ "visibility": project.visibility,
+ "archived": project.archived.unwrap_or(false),
+ "members": [] // Will be populated separately
+ }),
+ parents: vec![CedarParent {
+ parent_type: "Namespace".to_string(),
+ id: project.namespace.id.to_string(),
+ }],
+ }
+ }
+
+ // Build Group/Namespace entity
+ pub fn build_namespace(namespace: &crate::gitlab::Namespace) -> CedarEntity {
+ let mut parents = vec![];
+ if let Some(parent_id) = namespace.parent_id {
+ parents.push(CedarParent {
+ parent_type: "Namespace".to_string(),
+ id: parent_id.to_string(),
+ });
+ }
+
+ CedarEntity {
+ uid: CedarUid {
+ entity_type: "Namespace".to_string(),
+ id: namespace.id.to_string(),
+ },
+ attrs: serde_json::json!({
+ "name": namespace.name,
+ "path": namespace.path,
+ "full_path": namespace.full_path,
+ "kind": namespace.kind,
+ "visibility_level": namespace.visibility_level
+ }),
+ parents,
+ }
+ }
+
+ // Build Membership entity - represents user access to projects/groups
+ pub fn build_project_membership(
+ user_id: u64,
+ project_id: u64,
+ member: &crate::gitlab::Member,
+ ) -> CedarEntity {
+ CedarEntity {
+ uid: CedarUid {
+ entity_type: "ProjectMembership".to_string(),
+ id: format!("{}:{}", user_id, project_id),
+ },
+ attrs: serde_json::json!({
+ "user_id": user_id,
+ "project_id": project_id,
+ "access_level": member.access_level,
+ "expires_at": member.expires_at
+ }),
+ parents: vec![
+ CedarParent {
+ parent_type: "User".to_string(),
+ id: user_id.to_string(),
+ },
+ CedarParent {
+ parent_type: "Project".to_string(),
+ id: project_id.to_string(),
+ },
+ ],
+ }
+ }
+
+ // Build Group Membership entity
+ pub fn build_group_membership(
+ user_id: u64,
+ group_id: u64,
+ member: &crate::gitlab::Member,
+ ) -> CedarEntity {
+ CedarEntity {
+ uid: CedarUid {
+ entity_type: "GroupMembership".to_string(),
+ id: format!("{}:{}", user_id, group_id),
+ },
+ attrs: serde_json::json!({
+ "user_id": user_id,
+ "group_id": group_id,
+ "access_level": member.access_level,
+ "expires_at": member.expires_at
+ }),
+ parents: vec![
+ CedarParent {
+ parent_type: "User".to_string(),
+ id: user_id.to_string(),
+ },
+ CedarParent {
+ parent_type: "Namespace".to_string(),
+ id: group_id.to_string(),
+ },
+ ],
+ }
+ }
+
+ // Build Issue entity
+ pub fn build_issue(issue: &crate::gitlab::Issue, project_id: u64) -> CedarEntity {
+ CedarEntity {
+ uid: CedarUid {
+ entity_type: "Issue".to_string(),
+ id: issue.id.to_string(),
+ },
+ attrs: serde_json::json!({
+ "iid": issue.iid,
+ "title": issue.title,
+ "state": issue.state,
+ "confidential": issue.confidential,
+ "author_id": issue.author.id,
+ "assignee_ids": issue.assignees.iter().map(|a| a.id).collect::<Vec<_>>(),
+ "created_at": issue.created_at,
+ "updated_at": issue.updated_at
+ }),
+ parents: vec![CedarParent {
+ parent_type: "Project".to_string(),
+ id: project_id.to_string(),
+ }],
+ }
+ }
+
+ // Build MergeRequest entity
+ pub fn build_merge_request(mr: &crate::gitlab::MergeRequest, project_id: u64) -> CedarEntity {
+ CedarEntity {
+ uid: CedarUid {
+ entity_type: "MergeRequest".to_string(),
+ id: mr.id.to_string(),
+ },
+ attrs: serde_json::json!({
+ "iid": mr.iid,
+ "title": mr.title,
+ "state": mr.state,
+ "merge_status": mr.merge_status,
+ "author_id": mr.author.id,
+ "assignee_id": mr.assignee.as_ref().map(|a| a.id),
+ "target_branch": mr.target_branch,
+ "source_branch": mr.source_branch,
+ "work_in_progress": mr.work_in_progress,
+ "created_at": mr.created_at,
+ "updated_at": mr.updated_at
+ }),
+ parents: vec![CedarParent {
+ parent_type: "Project".to_string(),
+ id: project_id.to_string(),
+ }],
+ }
+ }
+} \ No newline at end of file
diff --git a/src/gitlab/mod.rs b/src/gitlab/mod.rs
index e1993d81..e8b49e0b 100644
--- a/src/gitlab/mod.rs
+++ b/src/gitlab/mod.rs
@@ -3,9 +3,11 @@ pub mod group;
pub mod member;
pub mod namespace;
pub mod project;
+pub mod user;
pub use api::Api;
pub use group::Group;
pub use member::Member;
pub use namespace::Namespace;
pub use project::Project;
+pub use user::User;
diff --git a/src/gitlab/project.rs b/src/gitlab/project.rs
index ba88c2e3..92201af3 100644
--- a/src/gitlab/project.rs
+++ b/src/gitlab/project.rs
@@ -7,5 +7,8 @@ pub struct Project {
pub id: u64,
pub name: String,
pub path: String,
+ pub path_with_namespace: String,
pub namespace: Namespace,
+ pub visibility: String, // "public", "internal", "private"
+ pub archived: Option<bool>,
}
diff --git a/src/gitlab/user.rs b/src/gitlab/user.rs
new file mode 100644
index 00000000..44ed61c1
--- /dev/null
+++ b/src/gitlab/user.rs
@@ -0,0 +1,13 @@
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+pub struct User {
+ pub id: u64,
+ pub username: String,
+ pub name: String,
+ pub state: String, // "active", "blocked", etc.
+ pub admin: Option<bool>,
+ pub bot: Option<bool>,
+ pub external: Option<bool>,
+ pub created_at: String,
+}