@code-rag/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/auth/audit-log.d.ts +35 -0
- package/dist/auth/audit-log.js +110 -0
- package/dist/auth/audit-log.js.map +1 -0
- package/dist/auth/audit-log.test.d.ts +1 -0
- package/dist/auth/audit-log.test.js +261 -0
- package/dist/auth/audit-log.test.js.map +1 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oidc-provider.d.ts +49 -0
- package/dist/auth/oidc-provider.js +358 -0
- package/dist/auth/oidc-provider.js.map +1 -0
- package/dist/auth/oidc-provider.test.d.ts +1 -0
- package/dist/auth/oidc-provider.test.js +520 -0
- package/dist/auth/oidc-provider.test.js.map +1 -0
- package/dist/auth/rbac.d.ts +29 -0
- package/dist/auth/rbac.js +75 -0
- package/dist/auth/rbac.js.map +1 -0
- package/dist/auth/rbac.test.d.ts +1 -0
- package/dist/auth/rbac.test.js +224 -0
- package/dist/auth/rbac.test.js.map +1 -0
- package/dist/auth/saml-provider.d.ts +51 -0
- package/dist/auth/saml-provider.js +355 -0
- package/dist/auth/saml-provider.js.map +1 -0
- package/dist/auth/saml-provider.test.d.ts +1 -0
- package/dist/auth/saml-provider.test.js +422 -0
- package/dist/auth/saml-provider.test.js.map +1 -0
- package/dist/auth/types.d.ts +81 -0
- package/dist/auth/types.js +11 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/auth/types.test.d.ts +1 -0
- package/dist/auth/types.test.js +147 -0
- package/dist/auth/types.test.js.map +1 -0
- package/dist/backlog/ab-reference-scanner.d.ts +10 -0
- package/dist/backlog/ab-reference-scanner.js +22 -0
- package/dist/backlog/ab-reference-scanner.js.map +1 -0
- package/dist/backlog/ab-reference-scanner.test.d.ts +1 -0
- package/dist/backlog/ab-reference-scanner.test.js +83 -0
- package/dist/backlog/ab-reference-scanner.test.js.map +1 -0
- package/dist/backlog/azure-devops-provider.d.ts +59 -0
- package/dist/backlog/azure-devops-provider.js +283 -0
- package/dist/backlog/azure-devops-provider.js.map +1 -0
- package/dist/backlog/backlog-provider.d.ts +13 -0
- package/dist/backlog/backlog-provider.js +6 -0
- package/dist/backlog/backlog-provider.js.map +1 -0
- package/dist/backlog/backlog-provider.test.d.ts +1 -0
- package/dist/backlog/backlog-provider.test.js +426 -0
- package/dist/backlog/backlog-provider.test.js.map +1 -0
- package/dist/backlog/clickup-provider.d.ts +55 -0
- package/dist/backlog/clickup-provider.js +301 -0
- package/dist/backlog/clickup-provider.js.map +1 -0
- package/dist/backlog/clickup-provider.test.d.ts +1 -0
- package/dist/backlog/clickup-provider.test.js +426 -0
- package/dist/backlog/clickup-provider.test.js.map +1 -0
- package/dist/backlog/clickup-reference-scanner.d.ts +10 -0
- package/dist/backlog/clickup-reference-scanner.js +32 -0
- package/dist/backlog/clickup-reference-scanner.js.map +1 -0
- package/dist/backlog/clickup-reference-scanner.test.d.ts +1 -0
- package/dist/backlog/clickup-reference-scanner.test.js +92 -0
- package/dist/backlog/clickup-reference-scanner.test.js.map +1 -0
- package/dist/backlog/code-linker.d.ts +63 -0
- package/dist/backlog/code-linker.js +90 -0
- package/dist/backlog/code-linker.js.map +1 -0
- package/dist/backlog/code-linker.test.d.ts +1 -0
- package/dist/backlog/code-linker.test.js +325 -0
- package/dist/backlog/code-linker.test.js.map +1 -0
- package/dist/backlog/index.d.ts +14 -0
- package/dist/backlog/index.js +8 -0
- package/dist/backlog/index.js.map +1 -0
- package/dist/backlog/jira-provider.d.ts +60 -0
- package/dist/backlog/jira-provider.js +272 -0
- package/dist/backlog/jira-provider.js.map +1 -0
- package/dist/backlog/jira-provider.test.d.ts +1 -0
- package/dist/backlog/jira-provider.test.js +449 -0
- package/dist/backlog/jira-provider.test.js.map +1 -0
- package/dist/backlog/jira-reference-scanner.d.ts +11 -0
- package/dist/backlog/jira-reference-scanner.js +26 -0
- package/dist/backlog/jira-reference-scanner.js.map +1 -0
- package/dist/backlog/jira-reference-scanner.test.d.ts +1 -0
- package/dist/backlog/jira-reference-scanner.test.js +127 -0
- package/dist/backlog/jira-reference-scanner.test.js.map +1 -0
- package/dist/backlog/types.d.ts +22 -0
- package/dist/backlog/types.js +1 -0
- package/dist/backlog/types.js.map +1 -0
- package/dist/chunker/ast-chunker.d.ts +45 -0
- package/dist/chunker/ast-chunker.js +292 -0
- package/dist/chunker/ast-chunker.js.map +1 -0
- package/dist/chunker/ast-chunker.test.d.ts +1 -0
- package/dist/chunker/ast-chunker.test.js +391 -0
- package/dist/chunker/ast-chunker.test.js.map +1 -0
- package/dist/chunker/chunker.d.ts +8 -0
- package/dist/chunker/chunker.js +1 -0
- package/dist/chunker/chunker.js.map +1 -0
- package/dist/chunker/index.d.ts +3 -0
- package/dist/chunker/index.js +2 -0
- package/dist/chunker/index.js.map +1 -0
- package/dist/config/config-parser.d.ts +15 -0
- package/dist/config/config-parser.js +283 -0
- package/dist/config/config-parser.js.map +1 -0
- package/dist/config/config-parser.test.d.ts +1 -0
- package/dist/config/config-parser.test.js +699 -0
- package/dist/config/config-parser.test.js.map +1 -0
- package/dist/docs/confluence-provider.d.ts +121 -0
- package/dist/docs/confluence-provider.js +459 -0
- package/dist/docs/confluence-provider.js.map +1 -0
- package/dist/docs/confluence-provider.test.d.ts +1 -0
- package/dist/docs/confluence-provider.test.js +765 -0
- package/dist/docs/confluence-provider.test.js.map +1 -0
- package/dist/docs/index.d.ts +4 -0
- package/dist/docs/index.js +2 -0
- package/dist/docs/index.js.map +1 -0
- package/dist/docs/sharepoint-provider.d.ts +150 -0
- package/dist/docs/sharepoint-provider.js +637 -0
- package/dist/docs/sharepoint-provider.js.map +1 -0
- package/dist/docs/sharepoint-provider.test.d.ts +1 -0
- package/dist/docs/sharepoint-provider.test.js +873 -0
- package/dist/docs/sharepoint-provider.test.js.map +1 -0
- package/dist/embedding/bm25-index.d.ts +12 -0
- package/dist/embedding/bm25-index.js +89 -0
- package/dist/embedding/bm25-index.js.map +1 -0
- package/dist/embedding/bm25-index.test.d.ts +1 -0
- package/dist/embedding/bm25-index.test.js +289 -0
- package/dist/embedding/bm25-index.test.js.map +1 -0
- package/dist/embedding/hybrid-search.d.ts +13 -0
- package/dist/embedding/hybrid-search.js +124 -0
- package/dist/embedding/hybrid-search.js.map +1 -0
- package/dist/embedding/hybrid-search.test.d.ts +1 -0
- package/dist/embedding/hybrid-search.test.js +266 -0
- package/dist/embedding/hybrid-search.test.js.map +1 -0
- package/dist/embedding/index.d.ts +11 -0
- package/dist/embedding/index.js +7 -0
- package/dist/embedding/index.js.map +1 -0
- package/dist/embedding/lancedb-store.d.ts +21 -0
- package/dist/embedding/lancedb-store.js +172 -0
- package/dist/embedding/lancedb-store.js.map +1 -0
- package/dist/embedding/lancedb-store.test.d.ts +1 -0
- package/dist/embedding/lancedb-store.test.js +268 -0
- package/dist/embedding/lancedb-store.test.js.map +1 -0
- package/dist/embedding/model-lifecycle-manager.d.ts +83 -0
- package/dist/embedding/model-lifecycle-manager.js +419 -0
- package/dist/embedding/model-lifecycle-manager.js.map +1 -0
- package/dist/embedding/model-lifecycle-manager.test.d.ts +1 -0
- package/dist/embedding/model-lifecycle-manager.test.js +642 -0
- package/dist/embedding/model-lifecycle-manager.test.js.map +1 -0
- package/dist/embedding/ollama-embedding-provider.d.ts +16 -0
- package/dist/embedding/ollama-embedding-provider.js +74 -0
- package/dist/embedding/ollama-embedding-provider.js.map +1 -0
- package/dist/embedding/ollama-embedding-provider.test.d.ts +1 -0
- package/dist/embedding/ollama-embedding-provider.test.js +198 -0
- package/dist/embedding/ollama-embedding-provider.test.js.map +1 -0
- package/dist/embedding/openai-compatible-embedding-provider.d.ts +19 -0
- package/dist/embedding/openai-compatible-embedding-provider.js +108 -0
- package/dist/embedding/openai-compatible-embedding-provider.js.map +1 -0
- package/dist/embedding/openai-compatible-embedding-provider.test.d.ts +1 -0
- package/dist/embedding/openai-compatible-embedding-provider.test.js +456 -0
- package/dist/embedding/openai-compatible-embedding-provider.test.js.map +1 -0
- package/dist/embedding/qdrant-store.d.ts +28 -0
- package/dist/embedding/qdrant-store.js +174 -0
- package/dist/embedding/qdrant-store.js.map +1 -0
- package/dist/embedding/qdrant-store.test.d.ts +1 -0
- package/dist/embedding/qdrant-store.test.js +359 -0
- package/dist/embedding/qdrant-store.test.js.map +1 -0
- package/dist/enrichment/index.d.ts +4 -0
- package/dist/enrichment/index.js +2 -0
- package/dist/enrichment/index.js.map +1 -0
- package/dist/enrichment/nl-enricher.d.ts +16 -0
- package/dist/enrichment/nl-enricher.js +47 -0
- package/dist/enrichment/nl-enricher.js.map +1 -0
- package/dist/enrichment/nl-enricher.test.d.ts +1 -0
- package/dist/enrichment/nl-enricher.test.js +154 -0
- package/dist/enrichment/nl-enricher.test.js.map +1 -0
- package/dist/enrichment/ollama-client.d.ts +18 -0
- package/dist/enrichment/ollama-client.js +55 -0
- package/dist/enrichment/ollama-client.js.map +1 -0
- package/dist/enrichment/ollama-client.test.d.ts +1 -0
- package/dist/enrichment/ollama-client.test.js +129 -0
- package/dist/enrichment/ollama-client.test.js.map +1 -0
- package/dist/git/git-client.d.ts +22 -0
- package/dist/git/git-client.js +6 -0
- package/dist/git/git-client.js.map +1 -0
- package/dist/git/git-client.test.d.ts +1 -0
- package/dist/git/git-client.test.js +200 -0
- package/dist/git/git-client.test.js.map +1 -0
- package/dist/git/ignore-filter.d.ts +2 -0
- package/dist/git/ignore-filter.js +31 -0
- package/dist/git/ignore-filter.js.map +1 -0
- package/dist/git/ignore-filter.test.d.ts +1 -0
- package/dist/git/ignore-filter.test.js +87 -0
- package/dist/git/ignore-filter.test.js.map +1 -0
- package/dist/git/index.d.ts +4 -0
- package/dist/git/index.js +3 -0
- package/dist/git/index.js.map +1 -0
- package/dist/git/simple-git-client.d.ts +12 -0
- package/dist/git/simple-git-client.js +138 -0
- package/dist/git/simple-git-client.js.map +1 -0
- package/dist/graph/cross-repo-resolver.d.ts +50 -0
- package/dist/graph/cross-repo-resolver.js +315 -0
- package/dist/graph/cross-repo-resolver.js.map +1 -0
- package/dist/graph/cross-repo-resolver.test.d.ts +1 -0
- package/dist/graph/cross-repo-resolver.test.js +548 -0
- package/dist/graph/cross-repo-resolver.test.js.map +1 -0
- package/dist/graph/dependency-graph.d.ts +44 -0
- package/dist/graph/dependency-graph.js +108 -0
- package/dist/graph/dependency-graph.js.map +1 -0
- package/dist/graph/dependency-graph.test.d.ts +1 -0
- package/dist/graph/dependency-graph.test.js +276 -0
- package/dist/graph/dependency-graph.test.js.map +1 -0
- package/dist/graph/graph-builder.d.ts +11 -0
- package/dist/graph/graph-builder.js +113 -0
- package/dist/graph/graph-builder.js.map +1 -0
- package/dist/graph/graph-builder.test.d.ts +1 -0
- package/dist/graph/graph-builder.test.js +178 -0
- package/dist/graph/graph-builder.test.js.map +1 -0
- package/dist/graph/import-resolver.d.ts +11 -0
- package/dist/graph/import-resolver.js +199 -0
- package/dist/graph/import-resolver.js.map +1 -0
- package/dist/graph/import-resolver.test.d.ts +1 -0
- package/dist/graph/import-resolver.test.js +282 -0
- package/dist/graph/import-resolver.test.js.map +1 -0
- package/dist/graph/index.d.ts +7 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/file-scanner.d.ts +34 -0
- package/dist/indexer/file-scanner.js +69 -0
- package/dist/indexer/file-scanner.js.map +1 -0
- package/dist/indexer/file-scanner.test.d.ts +1 -0
- package/dist/indexer/file-scanner.test.js +110 -0
- package/dist/indexer/file-scanner.test.js.map +1 -0
- package/dist/indexer/file-watcher.d.ts +79 -0
- package/dist/indexer/file-watcher.js +148 -0
- package/dist/indexer/incremental-indexer.d.ts +67 -0
- package/dist/indexer/incremental-indexer.js +142 -0
- package/dist/indexer/incremental-indexer.js.map +1 -0
- package/dist/indexer/incremental-indexer.test.d.ts +1 -0
- package/dist/indexer/incremental-indexer.test.js +266 -0
- package/dist/indexer/incremental-indexer.test.js.map +1 -0
- package/dist/indexer/index-check.d.ts +22 -0
- package/dist/indexer/index-check.js +74 -0
- package/dist/indexer/index-check.js.map +1 -0
- package/dist/indexer/index-check.test.d.ts +1 -0
- package/dist/indexer/index-check.test.js +100 -0
- package/dist/indexer/index-check.test.js.map +1 -0
- package/dist/indexer/index-state.d.ts +61 -0
- package/dist/indexer/index-state.js +82 -0
- package/dist/indexer/index-state.js.map +1 -0
- package/dist/indexer/index-state.test.d.ts +1 -0
- package/dist/indexer/index-state.test.js +140 -0
- package/dist/indexer/index-state.test.js.map +1 -0
- package/dist/indexer/index.d.ts +12 -0
- package/dist/indexer/index.js +6 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/multi-repo-indexer.d.ts +63 -0
- package/dist/indexer/multi-repo-indexer.js +144 -0
- package/dist/indexer/multi-repo-indexer.js.map +1 -0
- package/dist/indexer/multi-repo-indexer.test.d.ts +1 -0
- package/dist/indexer/multi-repo-indexer.test.js +238 -0
- package/dist/indexer/multi-repo-indexer.test.js.map +1 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +3 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/language-registry.d.ts +46 -0
- package/dist/parser/language-registry.js +219 -0
- package/dist/parser/language-registry.js.map +1 -0
- package/dist/parser/language-registry.test.d.ts +1 -0
- package/dist/parser/language-registry.test.js +225 -0
- package/dist/parser/language-registry.test.js.map +1 -0
- package/dist/parser/markdown-parser.d.ts +124 -0
- package/dist/parser/markdown-parser.js +487 -0
- package/dist/parser/markdown-parser.js.map +1 -0
- package/dist/parser/markdown-parser.test.d.ts +1 -0
- package/dist/parser/markdown-parser.test.js +600 -0
- package/dist/parser/markdown-parser.test.js.map +1 -0
- package/dist/parser/tree-sitter-parser.d.ts +32 -0
- package/dist/parser/tree-sitter-parser.js +146 -0
- package/dist/parser/tree-sitter-parser.js.map +1 -0
- package/dist/retrieval/context-expander.d.ts +51 -0
- package/dist/retrieval/context-expander.js +218 -0
- package/dist/retrieval/context-expander.js.map +1 -0
- package/dist/retrieval/context-expander.test.d.ts +1 -0
- package/dist/retrieval/context-expander.test.js +339 -0
- package/dist/retrieval/context-expander.test.js.map +1 -0
- package/dist/retrieval/cross-encoder-reranker.d.ts +16 -0
- package/dist/retrieval/cross-encoder-reranker.js +90 -0
- package/dist/retrieval/cross-encoder-reranker.js.map +1 -0
- package/dist/retrieval/cross-encoder-reranker.test.d.ts +1 -0
- package/dist/retrieval/cross-encoder-reranker.test.js +305 -0
- package/dist/retrieval/cross-encoder-reranker.test.js.map +1 -0
- package/dist/retrieval/index.d.ts +8 -0
- package/dist/retrieval/index.js +4 -0
- package/dist/retrieval/index.js.map +1 -0
- package/dist/retrieval/query-analyzer.d.ts +29 -0
- package/dist/retrieval/query-analyzer.js +238 -0
- package/dist/retrieval/query-analyzer.js.map +1 -0
- package/dist/retrieval/query-analyzer.test.d.ts +1 -0
- package/dist/retrieval/query-analyzer.test.js +236 -0
- package/dist/retrieval/query-analyzer.test.js.map +1 -0
- package/dist/retrieval/token-budget.d.ts +51 -0
- package/dist/retrieval/token-budget.js +141 -0
- package/dist/retrieval/token-budget.js.map +1 -0
- package/dist/retrieval/token-budget.test.d.ts +1 -0
- package/dist/retrieval/token-budget.test.js +404 -0
- package/dist/retrieval/token-budget.test.js.map +1 -0
- package/dist/storage/azure-blob-provider.d.ts +19 -0
- package/dist/storage/azure-blob-provider.js +199 -0
- package/dist/storage/azure-blob-provider.js.map +1 -0
- package/dist/storage/azure-blob-provider.test.d.ts +1 -0
- package/dist/storage/azure-blob-provider.test.js +250 -0
- package/dist/storage/azure-blob-provider.test.js.map +1 -0
- package/dist/storage/gcs-provider.d.ts +22 -0
- package/dist/storage/gcs-provider.js +241 -0
- package/dist/storage/gcs-provider.js.map +1 -0
- package/dist/storage/gcs-provider.test.d.ts +1 -0
- package/dist/storage/gcs-provider.test.js +299 -0
- package/dist/storage/gcs-provider.test.js.map +1 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/s3-provider.d.ts +21 -0
- package/dist/storage/s3-provider.js +220 -0
- package/dist/storage/s3-provider.js.map +1 -0
- package/dist/storage/s3-provider.test.d.ts +1 -0
- package/dist/storage/s3-provider.test.js +329 -0
- package/dist/storage/s3-provider.test.js.map +1 -0
- package/dist/storage/types.d.ts +65 -0
- package/dist/storage/types.js +12 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/types/chunk.d.ts +32 -0
- package/dist/types/chunk.js +1 -0
- package/dist/types/chunk.js.map +1 -0
- package/dist/types/config.d.ts +71 -0
- package/dist/types/config.js +1 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/provider.d.ts +54 -0
- package/dist/types/provider.js +36 -0
- package/dist/types/provider.js.map +1 -0
- package/dist/types/search.d.ts +27 -0
- package/dist/types/search.js +1 -0
- package/dist/types/search.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { RBACManager } from './rbac.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function createUser(overrides) {
|
|
7
|
+
return {
|
|
8
|
+
id: 'user-1',
|
|
9
|
+
email: 'dev@example.com',
|
|
10
|
+
name: 'Test User',
|
|
11
|
+
roles: ['viewer'],
|
|
12
|
+
allowedRepos: ['repo-a', 'repo-b'],
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function createSearchResult(repoName) {
|
|
17
|
+
return {
|
|
18
|
+
chunkId: 'chunk-1',
|
|
19
|
+
content: 'function foo() {}',
|
|
20
|
+
nlSummary: 'A function named foo',
|
|
21
|
+
score: 0.95,
|
|
22
|
+
method: 'hybrid',
|
|
23
|
+
metadata: {
|
|
24
|
+
chunkType: 'function',
|
|
25
|
+
name: 'foo',
|
|
26
|
+
declarations: [],
|
|
27
|
+
imports: [],
|
|
28
|
+
exports: [],
|
|
29
|
+
repoName,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Tests
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
describe('RBACManager', () => {
|
|
37
|
+
const rbac = new RBACManager();
|
|
38
|
+
// -----------------------------------------------------------------------
|
|
39
|
+
// hasPermission
|
|
40
|
+
// -----------------------------------------------------------------------
|
|
41
|
+
describe('hasPermission', () => {
|
|
42
|
+
it('should allow viewer to search', () => {
|
|
43
|
+
const user = createUser({ roles: ['viewer'] });
|
|
44
|
+
expect(rbac.hasPermission(user, 'search')).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('should allow viewer to access context', () => {
|
|
47
|
+
const user = createUser({ roles: ['viewer'] });
|
|
48
|
+
expect(rbac.hasPermission(user, 'context')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('should allow viewer to access status', () => {
|
|
51
|
+
const user = createUser({ roles: ['viewer'] });
|
|
52
|
+
expect(rbac.hasPermission(user, 'status')).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it('should deny viewer from explain', () => {
|
|
55
|
+
const user = createUser({ roles: ['viewer'] });
|
|
56
|
+
expect(rbac.hasPermission(user, 'explain')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it('should deny viewer from docs', () => {
|
|
59
|
+
const user = createUser({ roles: ['viewer'] });
|
|
60
|
+
expect(rbac.hasPermission(user, 'docs')).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
it('should deny viewer from index', () => {
|
|
63
|
+
const user = createUser({ roles: ['viewer'] });
|
|
64
|
+
expect(rbac.hasPermission(user, 'index')).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
it('should deny viewer from configure', () => {
|
|
67
|
+
const user = createUser({ roles: ['viewer'] });
|
|
68
|
+
expect(rbac.hasPermission(user, 'configure')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
it('should allow developer to explain', () => {
|
|
71
|
+
const user = createUser({ roles: ['developer'] });
|
|
72
|
+
expect(rbac.hasPermission(user, 'explain')).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
it('should allow developer to access docs', () => {
|
|
75
|
+
const user = createUser({ roles: ['developer'] });
|
|
76
|
+
expect(rbac.hasPermission(user, 'docs')).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
it('should deny developer from index', () => {
|
|
79
|
+
const user = createUser({ roles: ['developer'] });
|
|
80
|
+
expect(rbac.hasPermission(user, 'index')).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
it('should deny developer from configure', () => {
|
|
83
|
+
const user = createUser({ roles: ['developer'] });
|
|
84
|
+
expect(rbac.hasPermission(user, 'configure')).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
it('should allow admin to perform all actions', () => {
|
|
87
|
+
const user = createUser({ roles: ['admin'] });
|
|
88
|
+
const allActions = [
|
|
89
|
+
'search', 'context', 'status', 'explain', 'docs', 'index', 'configure',
|
|
90
|
+
];
|
|
91
|
+
for (const action of allActions) {
|
|
92
|
+
expect(rbac.hasPermission(user, action)).toBe(true);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
it('should allow action if any role permits it', () => {
|
|
96
|
+
const user = createUser({ roles: ['viewer', 'developer'] });
|
|
97
|
+
expect(rbac.hasPermission(user, 'explain')).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it('should deny action if no role permits it', () => {
|
|
100
|
+
const user = createUser({ roles: ['viewer', 'developer'] });
|
|
101
|
+
expect(rbac.hasPermission(user, 'index')).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
// -----------------------------------------------------------------------
|
|
105
|
+
// canAccessRepo
|
|
106
|
+
// -----------------------------------------------------------------------
|
|
107
|
+
describe('canAccessRepo', () => {
|
|
108
|
+
it('should allow access when repo is in allowedRepos', () => {
|
|
109
|
+
const user = createUser({ allowedRepos: ['repo-a', 'repo-b'] });
|
|
110
|
+
expect(rbac.canAccessRepo(user, 'repo-a')).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it('should deny access when repo is not in allowedRepos', () => {
|
|
113
|
+
const user = createUser({ allowedRepos: ['repo-a'] });
|
|
114
|
+
expect(rbac.canAccessRepo(user, 'repo-c')).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
it('should grant admin with empty allowedRepos access to all repos', () => {
|
|
117
|
+
const user = createUser({ roles: ['admin'], allowedRepos: [] });
|
|
118
|
+
expect(rbac.canAccessRepo(user, 'any-repo')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
it('should restrict non-admin with empty allowedRepos', () => {
|
|
121
|
+
const user = createUser({ roles: ['viewer'], allowedRepos: [] });
|
|
122
|
+
expect(rbac.canAccessRepo(user, 'any-repo')).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
it('should restrict admin with explicit allowedRepos to those repos', () => {
|
|
125
|
+
const user = createUser({
|
|
126
|
+
roles: ['admin'],
|
|
127
|
+
allowedRepos: ['repo-x'],
|
|
128
|
+
});
|
|
129
|
+
expect(rbac.canAccessRepo(user, 'repo-x')).toBe(true);
|
|
130
|
+
expect(rbac.canAccessRepo(user, 'repo-y')).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
// -----------------------------------------------------------------------
|
|
134
|
+
// filterResultsByAccess
|
|
135
|
+
// -----------------------------------------------------------------------
|
|
136
|
+
describe('filterResultsByAccess', () => {
|
|
137
|
+
it('should keep results from allowed repos', () => {
|
|
138
|
+
const user = createUser({ allowedRepos: ['repo-a'] });
|
|
139
|
+
const results = [
|
|
140
|
+
createSearchResult('repo-a'),
|
|
141
|
+
createSearchResult('repo-b'),
|
|
142
|
+
];
|
|
143
|
+
const filtered = rbac.filterResultsByAccess(user, results);
|
|
144
|
+
expect(filtered).toHaveLength(1);
|
|
145
|
+
expect(filtered[0].metadata.repoName).toBe('repo-a');
|
|
146
|
+
});
|
|
147
|
+
it('should keep results without repoName (single-repo mode)', () => {
|
|
148
|
+
const user = createUser({ allowedRepos: ['repo-a'] });
|
|
149
|
+
const results = [
|
|
150
|
+
createSearchResult(undefined),
|
|
151
|
+
createSearchResult('repo-b'),
|
|
152
|
+
];
|
|
153
|
+
const filtered = rbac.filterResultsByAccess(user, results);
|
|
154
|
+
expect(filtered).toHaveLength(1);
|
|
155
|
+
expect(filtered[0].metadata.repoName).toBeUndefined();
|
|
156
|
+
});
|
|
157
|
+
it('should return all results for admin with empty allowedRepos', () => {
|
|
158
|
+
const user = createUser({ roles: ['admin'], allowedRepos: [] });
|
|
159
|
+
const results = [
|
|
160
|
+
createSearchResult('repo-a'),
|
|
161
|
+
createSearchResult('repo-b'),
|
|
162
|
+
createSearchResult('repo-c'),
|
|
163
|
+
];
|
|
164
|
+
const filtered = rbac.filterResultsByAccess(user, results);
|
|
165
|
+
expect(filtered).toHaveLength(3);
|
|
166
|
+
});
|
|
167
|
+
it('should return empty array when no results match', () => {
|
|
168
|
+
const user = createUser({ allowedRepos: ['repo-x'] });
|
|
169
|
+
const results = [
|
|
170
|
+
createSearchResult('repo-a'),
|
|
171
|
+
createSearchResult('repo-b'),
|
|
172
|
+
];
|
|
173
|
+
const filtered = rbac.filterResultsByAccess(user, results);
|
|
174
|
+
expect(filtered).toHaveLength(0);
|
|
175
|
+
});
|
|
176
|
+
it('should not mutate the original results array', () => {
|
|
177
|
+
const user = createUser({ allowedRepos: ['repo-a'] });
|
|
178
|
+
const results = [
|
|
179
|
+
createSearchResult('repo-a'),
|
|
180
|
+
createSearchResult('repo-b'),
|
|
181
|
+
];
|
|
182
|
+
const originalLength = results.length;
|
|
183
|
+
rbac.filterResultsByAccess(user, results);
|
|
184
|
+
expect(results).toHaveLength(originalLength);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
// -----------------------------------------------------------------------
|
|
188
|
+
// getRoleLevel & getHighestRole
|
|
189
|
+
// -----------------------------------------------------------------------
|
|
190
|
+
describe('getRoleLevel', () => {
|
|
191
|
+
it('should return 0 for viewer', () => {
|
|
192
|
+
expect(rbac.getRoleLevel('viewer')).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
it('should return 1 for developer', () => {
|
|
195
|
+
expect(rbac.getRoleLevel('developer')).toBe(1);
|
|
196
|
+
});
|
|
197
|
+
it('should return 2 for admin', () => {
|
|
198
|
+
expect(rbac.getRoleLevel('admin')).toBe(2);
|
|
199
|
+
});
|
|
200
|
+
it('should maintain hierarchy ordering', () => {
|
|
201
|
+
expect(rbac.getRoleLevel('admin')).toBeGreaterThan(rbac.getRoleLevel('developer'));
|
|
202
|
+
expect(rbac.getRoleLevel('developer')).toBeGreaterThan(rbac.getRoleLevel('viewer'));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe('getHighestRole', () => {
|
|
206
|
+
it('should return admin when user has admin role', () => {
|
|
207
|
+
const user = createUser({ roles: ['viewer', 'admin'] });
|
|
208
|
+
expect(rbac.getHighestRole(user)).toBe('admin');
|
|
209
|
+
});
|
|
210
|
+
it('should return developer when user has developer but not admin', () => {
|
|
211
|
+
const user = createUser({ roles: ['viewer', 'developer'] });
|
|
212
|
+
expect(rbac.getHighestRole(user)).toBe('developer');
|
|
213
|
+
});
|
|
214
|
+
it('should return viewer when that is the only role', () => {
|
|
215
|
+
const user = createUser({ roles: ['viewer'] });
|
|
216
|
+
expect(rbac.getHighestRole(user)).toBe('viewer');
|
|
217
|
+
});
|
|
218
|
+
it('should return admin when all roles are present', () => {
|
|
219
|
+
const user = createUser({ roles: ['viewer', 'developer', 'admin'] });
|
|
220
|
+
expect(rbac.getHighestRole(user)).toBe('admin');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
//# sourceMappingURL=rbac.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rbac.test.js","sourceRoot":"","sources":["../../src/auth/rbac.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Result } from 'neverthrow';
|
|
2
|
+
import type { AuthProvider, AuthToken, Role, SAMLConfig, User } from './types.js';
|
|
3
|
+
import { AuthError } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* SAML 2.0 `AuthProvider` implementation.
|
|
6
|
+
*
|
|
7
|
+
* Handles AuthnRequest generation and SAML Response validation for
|
|
8
|
+
* enterprise SSO integration.
|
|
9
|
+
*/
|
|
10
|
+
export declare class SAMLProvider implements AuthProvider {
|
|
11
|
+
readonly name = "saml";
|
|
12
|
+
private readonly config;
|
|
13
|
+
private idpMetadata;
|
|
14
|
+
/** Users whose info has been resolved (in-memory cache). */
|
|
15
|
+
private readonly userCache;
|
|
16
|
+
/** Counter for unique AuthnRequest IDs. */
|
|
17
|
+
private requestCounter;
|
|
18
|
+
/**
|
|
19
|
+
* Pluggable `fetch` function — defaults to the global `fetch`.
|
|
20
|
+
*/
|
|
21
|
+
private readonly fetchFn;
|
|
22
|
+
constructor(config: SAMLConfig, fetchFn?: typeof fetch);
|
|
23
|
+
/**
|
|
24
|
+
* Fetches and parses IdP metadata XML to discover SSO URL, certificate,
|
|
25
|
+
* and NameID format.
|
|
26
|
+
*/
|
|
27
|
+
initialize(): Promise<Result<void, AuthError>>;
|
|
28
|
+
/**
|
|
29
|
+
* Creates a SAML AuthnRequest and returns the IdP redirect URL together
|
|
30
|
+
* with the request ID (for later response correlation).
|
|
31
|
+
*/
|
|
32
|
+
generateAuthRequest(): Result<{
|
|
33
|
+
url: string;
|
|
34
|
+
id: string;
|
|
35
|
+
}, AuthError>;
|
|
36
|
+
authenticate(token: string): Promise<Result<AuthToken, AuthError>>;
|
|
37
|
+
getUserRoles(userId: string): Promise<Result<readonly Role[], AuthError>>;
|
|
38
|
+
getUserRepos(userId: string): Promise<Result<readonly string[], AuthError>>;
|
|
39
|
+
/**
|
|
40
|
+
* Validates a Base64-encoded SAML Response: checks XML signature,
|
|
41
|
+
* conditions (audience, timestamps), and extracts the user.
|
|
42
|
+
*/
|
|
43
|
+
validateResponse(samlResponseB64: string): Promise<Result<User, AuthError>>;
|
|
44
|
+
/**
|
|
45
|
+
* Maps SAML attributes to a CodeRAG `User`.
|
|
46
|
+
*/
|
|
47
|
+
mapAttributes(attributes: Readonly<Record<string, string>>, xml?: string): User;
|
|
48
|
+
private mapRoleValues;
|
|
49
|
+
private verifyXmlSignature;
|
|
50
|
+
private checkConditions;
|
|
51
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { ok, err } from 'neverthrow';
|
|
2
|
+
import { createVerify } from 'node:crypto';
|
|
3
|
+
import { AuthError } from './types.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Extracts the text content of the first occurrence of `tagName` from XML.
|
|
9
|
+
* This is a minimal, dependency-free XML "parser" — it does **not** handle
|
|
10
|
+
* namespaces, CDATA, or nested elements with the same local name. Sufficient
|
|
11
|
+
* for SAML metadata / response parsing.
|
|
12
|
+
*/
|
|
13
|
+
function xmlGetText(xml, tagName) {
|
|
14
|
+
// Match both with and without namespace prefix
|
|
15
|
+
const localName = tagName.includes(':') ? tagName.split(':').pop() : tagName;
|
|
16
|
+
// Try with namespace prefix first, then without
|
|
17
|
+
for (const name of [tagName, localName]) {
|
|
18
|
+
const esc = escapeRegex(name);
|
|
19
|
+
const patterns = [
|
|
20
|
+
// <ns:Tag ...>content</ns:Tag> — requires tag name to end at > or whitespace
|
|
21
|
+
new RegExp(`<(?:[a-zA-Z0-9_]+:)?${esc}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/(?:[a-zA-Z0-9_]+:)?${esc}>`, 'i'),
|
|
22
|
+
// <Tag ...>content</Tag>
|
|
23
|
+
new RegExp(`<${esc}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${esc}>`, 'i'),
|
|
24
|
+
];
|
|
25
|
+
for (const re of patterns) {
|
|
26
|
+
const match = re.exec(xml);
|
|
27
|
+
if (match?.[1] !== undefined) {
|
|
28
|
+
return match[1].trim();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extracts an attribute value from the first element matching `tagName`.
|
|
36
|
+
*/
|
|
37
|
+
function xmlGetAttr(xml, tagName, attrName) {
|
|
38
|
+
const localName = tagName.includes(':') ? tagName.split(':').pop() : tagName;
|
|
39
|
+
for (const name of [tagName, localName]) {
|
|
40
|
+
const esc = escapeRegex(name);
|
|
41
|
+
// Match <ns:Tag ...> or <Tag ...> — tag name must be followed by whitespace, /, or >
|
|
42
|
+
const tagRe = new RegExp(`<(?:[a-zA-Z0-9_]+:)?${esc}(?:\\s[^>]*|\\/)?>`, 'i');
|
|
43
|
+
const tagMatch = tagRe.exec(xml);
|
|
44
|
+
if (tagMatch) {
|
|
45
|
+
const attrRe = new RegExp(`${escapeRegex(attrName)}\\s*=\\s*"([^"]*)"`, 'i');
|
|
46
|
+
const attrMatch = attrRe.exec(tagMatch[0]);
|
|
47
|
+
if (attrMatch?.[1] !== undefined) {
|
|
48
|
+
return attrMatch[1];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extracts all SAML attribute values from a SAML Response.
|
|
56
|
+
* Returns a map of attribute name -> value.
|
|
57
|
+
*/
|
|
58
|
+
function extractSamlAttributes(xml) {
|
|
59
|
+
const attrs = {};
|
|
60
|
+
// Match <saml:Attribute Name="..."><saml:AttributeValue>...</saml:AttributeValue></saml:Attribute>
|
|
61
|
+
const attrRe = /<[^>]*?Attribute\s[^>]*?Name\s*=\s*"([^"]*)"[^>]*?>([\s\S]*?)<\/[^>]*?Attribute>/gi;
|
|
62
|
+
let attrMatch = attrRe.exec(xml);
|
|
63
|
+
while (attrMatch) {
|
|
64
|
+
const name = attrMatch[1];
|
|
65
|
+
const body = attrMatch[2];
|
|
66
|
+
if (name && body) {
|
|
67
|
+
// Get the first AttributeValue
|
|
68
|
+
const valueRe = /<[^>]*?AttributeValue[^>]*?>([\s\S]*?)<\/[^>]*?AttributeValue>/i;
|
|
69
|
+
const valueMatch = valueRe.exec(body);
|
|
70
|
+
if (valueMatch?.[1] !== undefined) {
|
|
71
|
+
attrs[name] = valueMatch[1].trim();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
attrMatch = attrRe.exec(xml);
|
|
75
|
+
}
|
|
76
|
+
return attrs;
|
|
77
|
+
}
|
|
78
|
+
function escapeRegex(str) {
|
|
79
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// SAMLProvider
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
/**
|
|
85
|
+
* SAML 2.0 `AuthProvider` implementation.
|
|
86
|
+
*
|
|
87
|
+
* Handles AuthnRequest generation and SAML Response validation for
|
|
88
|
+
* enterprise SSO integration.
|
|
89
|
+
*/
|
|
90
|
+
export class SAMLProvider {
|
|
91
|
+
name = 'saml';
|
|
92
|
+
config;
|
|
93
|
+
idpMetadata;
|
|
94
|
+
/** Users whose info has been resolved (in-memory cache). */
|
|
95
|
+
userCache = new Map();
|
|
96
|
+
/** Counter for unique AuthnRequest IDs. */
|
|
97
|
+
requestCounter = 0;
|
|
98
|
+
/**
|
|
99
|
+
* Pluggable `fetch` function — defaults to the global `fetch`.
|
|
100
|
+
*/
|
|
101
|
+
fetchFn;
|
|
102
|
+
constructor(config, fetchFn) {
|
|
103
|
+
this.config = config;
|
|
104
|
+
this.fetchFn = fetchFn ?? globalThis.fetch;
|
|
105
|
+
}
|
|
106
|
+
// -----------------------------------------------------------------------
|
|
107
|
+
// Initialization
|
|
108
|
+
// -----------------------------------------------------------------------
|
|
109
|
+
/**
|
|
110
|
+
* Fetches and parses IdP metadata XML to discover SSO URL, certificate,
|
|
111
|
+
* and NameID format.
|
|
112
|
+
*/
|
|
113
|
+
async initialize() {
|
|
114
|
+
try {
|
|
115
|
+
const response = await this.fetchFn(this.config.idpMetadataUrl);
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
return err(new AuthError(`IdP metadata fetch failed: HTTP ${String(response.status)}`));
|
|
118
|
+
}
|
|
119
|
+
const xml = await response.text();
|
|
120
|
+
const entityId = xmlGetAttr(xml, 'EntityDescriptor', 'entityID') ?? '';
|
|
121
|
+
const ssoUrl = xmlGetAttr(xml, 'SingleSignOnService', 'Location') ?? '';
|
|
122
|
+
const certificate = xmlGetText(xml, 'X509Certificate') ?? '';
|
|
123
|
+
const nameIdFormat = xmlGetText(xml, 'NameIDFormat') ??
|
|
124
|
+
'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress';
|
|
125
|
+
if (!entityId || !ssoUrl || !certificate) {
|
|
126
|
+
return err(new AuthError('IdP metadata missing required fields (entityID, SSO URL, or certificate)'));
|
|
127
|
+
}
|
|
128
|
+
this.idpMetadata = { entityId, ssoUrl, certificate, nameIdFormat };
|
|
129
|
+
return ok(undefined);
|
|
130
|
+
}
|
|
131
|
+
catch (cause) {
|
|
132
|
+
const message = cause instanceof Error ? cause.message : 'Unknown error';
|
|
133
|
+
return err(new AuthError(`SAML initialization error: ${message}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
// AuthnRequest generation
|
|
138
|
+
// -----------------------------------------------------------------------
|
|
139
|
+
/**
|
|
140
|
+
* Creates a SAML AuthnRequest and returns the IdP redirect URL together
|
|
141
|
+
* with the request ID (for later response correlation).
|
|
142
|
+
*/
|
|
143
|
+
generateAuthRequest() {
|
|
144
|
+
if (!this.idpMetadata) {
|
|
145
|
+
return err(new AuthError('SAML not initialized — call initialize() first'));
|
|
146
|
+
}
|
|
147
|
+
this.requestCounter += 1;
|
|
148
|
+
const id = `_coderag_${Date.now()}_${String(this.requestCounter)}`;
|
|
149
|
+
const issueInstant = new Date().toISOString();
|
|
150
|
+
const authnRequest = [
|
|
151
|
+
'<samlp:AuthnRequest',
|
|
152
|
+
' xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"',
|
|
153
|
+
' xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"',
|
|
154
|
+
` ID="${id}"`,
|
|
155
|
+
' Version="2.0"',
|
|
156
|
+
` IssueInstant="${issueInstant}"`,
|
|
157
|
+
` Destination="${this.idpMetadata.ssoUrl}"`,
|
|
158
|
+
` AssertionConsumerServiceURL="${this.config.spAcsUrl}"`,
|
|
159
|
+
` ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">`,
|
|
160
|
+
` <saml:Issuer>${this.config.spEntityId}</saml:Issuer>`,
|
|
161
|
+
' <samlp:NameIDPolicy',
|
|
162
|
+
` Format="${this.idpMetadata.nameIdFormat}"`,
|
|
163
|
+
' AllowCreate="true" />',
|
|
164
|
+
'</samlp:AuthnRequest>',
|
|
165
|
+
].join('\n');
|
|
166
|
+
const encoded = Buffer.from(authnRequest).toString('base64');
|
|
167
|
+
const separator = this.idpMetadata.ssoUrl.includes('?') ? '&' : '?';
|
|
168
|
+
const url = `${this.idpMetadata.ssoUrl}${separator}SAMLRequest=${encodeURIComponent(encoded)}`;
|
|
169
|
+
return ok({ url, id });
|
|
170
|
+
}
|
|
171
|
+
// -----------------------------------------------------------------------
|
|
172
|
+
// AuthProvider implementation
|
|
173
|
+
// -----------------------------------------------------------------------
|
|
174
|
+
async authenticate(token) {
|
|
175
|
+
const userResult = await this.validateResponse(token);
|
|
176
|
+
if (userResult.isErr()) {
|
|
177
|
+
return err(userResult.error);
|
|
178
|
+
}
|
|
179
|
+
const user = userResult.value;
|
|
180
|
+
const now = Math.floor(Date.now() / 1000);
|
|
181
|
+
const authToken = {
|
|
182
|
+
userId: user.id,
|
|
183
|
+
email: user.email,
|
|
184
|
+
roles: user.roles,
|
|
185
|
+
exp: now + 3600, // 1 hour default
|
|
186
|
+
iat: now,
|
|
187
|
+
};
|
|
188
|
+
return ok(authToken);
|
|
189
|
+
}
|
|
190
|
+
async getUserRoles(userId) {
|
|
191
|
+
const user = this.userCache.get(userId);
|
|
192
|
+
if (user) {
|
|
193
|
+
return ok(user.roles);
|
|
194
|
+
}
|
|
195
|
+
return ok(['viewer']);
|
|
196
|
+
}
|
|
197
|
+
async getUserRepos(userId) {
|
|
198
|
+
const user = this.userCache.get(userId);
|
|
199
|
+
if (user) {
|
|
200
|
+
return ok(user.allowedRepos);
|
|
201
|
+
}
|
|
202
|
+
return ok([]);
|
|
203
|
+
}
|
|
204
|
+
// -----------------------------------------------------------------------
|
|
205
|
+
// SAML Response validation
|
|
206
|
+
// -----------------------------------------------------------------------
|
|
207
|
+
/**
|
|
208
|
+
* Validates a Base64-encoded SAML Response: checks XML signature,
|
|
209
|
+
* conditions (audience, timestamps), and extracts the user.
|
|
210
|
+
*/
|
|
211
|
+
async validateResponse(samlResponseB64) {
|
|
212
|
+
let xml;
|
|
213
|
+
try {
|
|
214
|
+
xml = Buffer.from(samlResponseB64, 'base64').toString('utf-8');
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return err(new AuthError('Invalid Base64 SAML response'));
|
|
218
|
+
}
|
|
219
|
+
// Verify signature
|
|
220
|
+
const sigResult = this.verifyXmlSignature(xml);
|
|
221
|
+
if (sigResult.isErr()) {
|
|
222
|
+
return err(sigResult.error);
|
|
223
|
+
}
|
|
224
|
+
// Check conditions
|
|
225
|
+
const condResult = this.checkConditions(xml);
|
|
226
|
+
if (condResult.isErr()) {
|
|
227
|
+
return err(condResult.error);
|
|
228
|
+
}
|
|
229
|
+
// Extract user
|
|
230
|
+
const user = this.mapAttributes(extractSamlAttributes(xml), xml);
|
|
231
|
+
this.userCache.set(user.id, user);
|
|
232
|
+
return ok(user);
|
|
233
|
+
}
|
|
234
|
+
// -----------------------------------------------------------------------
|
|
235
|
+
// Attribute mapping
|
|
236
|
+
// -----------------------------------------------------------------------
|
|
237
|
+
/**
|
|
238
|
+
* Maps SAML attributes to a CodeRAG `User`.
|
|
239
|
+
*/
|
|
240
|
+
mapAttributes(attributes, xml) {
|
|
241
|
+
const email = attributes['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] ??
|
|
242
|
+
attributes['email'] ??
|
|
243
|
+
attributes['Email'] ??
|
|
244
|
+
'';
|
|
245
|
+
const name = attributes['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'] ??
|
|
246
|
+
attributes['displayName'] ??
|
|
247
|
+
attributes['name'] ??
|
|
248
|
+
email;
|
|
249
|
+
// Extract NameID as user ID
|
|
250
|
+
let nameId = '';
|
|
251
|
+
if (xml) {
|
|
252
|
+
nameId = xmlGetText(xml, 'NameID') ?? '';
|
|
253
|
+
}
|
|
254
|
+
const id = nameId || email;
|
|
255
|
+
// Map roles
|
|
256
|
+
const roleAttr = attributes['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'] ??
|
|
257
|
+
attributes['role'] ??
|
|
258
|
+
attributes['Role'] ??
|
|
259
|
+
'';
|
|
260
|
+
const roles = this.mapRoleValues(roleAttr);
|
|
261
|
+
return { id, email, name, roles, allowedRepos: [] };
|
|
262
|
+
}
|
|
263
|
+
// -----------------------------------------------------------------------
|
|
264
|
+
// Private helpers
|
|
265
|
+
// -----------------------------------------------------------------------
|
|
266
|
+
mapRoleValues(roleValue) {
|
|
267
|
+
const mapping = this.config.roleMapping ?? {};
|
|
268
|
+
const values = roleValue
|
|
269
|
+
.split(',')
|
|
270
|
+
.map((v) => v.trim())
|
|
271
|
+
.filter(Boolean);
|
|
272
|
+
const roles = new Set();
|
|
273
|
+
for (const value of values) {
|
|
274
|
+
const mapped = mapping[value];
|
|
275
|
+
if (mapped) {
|
|
276
|
+
roles.add(mapped);
|
|
277
|
+
}
|
|
278
|
+
if (value === 'admin' || value === 'developer' || value === 'viewer') {
|
|
279
|
+
roles.add(value);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (roles.size === 0) {
|
|
283
|
+
return ['viewer'];
|
|
284
|
+
}
|
|
285
|
+
return [...roles];
|
|
286
|
+
}
|
|
287
|
+
verifyXmlSignature(xml) {
|
|
288
|
+
if (!this.idpMetadata) {
|
|
289
|
+
return err(new AuthError('SAML not initialized — call initialize() first'));
|
|
290
|
+
}
|
|
291
|
+
// Extract SignatureValue and signed content
|
|
292
|
+
const signatureValue = xmlGetText(xml, 'SignatureValue');
|
|
293
|
+
if (!signatureValue) {
|
|
294
|
+
return err(new AuthError('No SignatureValue found in SAML response'));
|
|
295
|
+
}
|
|
296
|
+
// Extract the signed content (the Assertion element)
|
|
297
|
+
const digestValue = xmlGetText(xml, 'DigestValue');
|
|
298
|
+
if (!digestValue) {
|
|
299
|
+
return err(new AuthError('No DigestValue found in SAML response'));
|
|
300
|
+
}
|
|
301
|
+
// Verify using the IdP certificate or public key
|
|
302
|
+
const signedInfo = xmlGetText(xml, 'SignedInfo');
|
|
303
|
+
if (!signedInfo) {
|
|
304
|
+
return err(new AuthError('No SignedInfo found in SAML response'));
|
|
305
|
+
}
|
|
306
|
+
// Reconstruct SignedInfo as canonical XML for verification
|
|
307
|
+
const signedInfoXml = `<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">${signedInfo}</SignedInfo>`;
|
|
308
|
+
const signature = Buffer.from(signatureValue.replace(/\s/g, ''), 'base64');
|
|
309
|
+
// Try certificate format first, then public key format
|
|
310
|
+
const keyFormats = [
|
|
311
|
+
`-----BEGIN CERTIFICATE-----\n${this.idpMetadata.certificate}\n-----END CERTIFICATE-----`,
|
|
312
|
+
`-----BEGIN PUBLIC KEY-----\n${this.idpMetadata.certificate}\n-----END PUBLIC KEY-----`,
|
|
313
|
+
];
|
|
314
|
+
for (const keyPem of keyFormats) {
|
|
315
|
+
try {
|
|
316
|
+
const verifier = createVerify('RSA-SHA256');
|
|
317
|
+
verifier.update(signedInfoXml);
|
|
318
|
+
const valid = verifier.verify(keyPem, signature);
|
|
319
|
+
if (valid) {
|
|
320
|
+
return ok(undefined);
|
|
321
|
+
}
|
|
322
|
+
return err(new AuthError('Invalid SAML response signature'));
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// Try next format
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return err(new AuthError('Signature verification failed: unsupported key format'));
|
|
330
|
+
}
|
|
331
|
+
checkConditions(xml) {
|
|
332
|
+
// Check NotBefore / NotOnOrAfter
|
|
333
|
+
const notBeforeStr = xmlGetAttr(xml, 'Conditions', 'NotBefore');
|
|
334
|
+
const notOnOrAfterStr = xmlGetAttr(xml, 'Conditions', 'NotOnOrAfter');
|
|
335
|
+
const now = new Date();
|
|
336
|
+
if (notBeforeStr) {
|
|
337
|
+
const notBefore = new Date(notBeforeStr);
|
|
338
|
+
if (now < notBefore) {
|
|
339
|
+
return err(new AuthError('SAML assertion not yet valid'));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (notOnOrAfterStr) {
|
|
343
|
+
const notOnOrAfter = new Date(notOnOrAfterStr);
|
|
344
|
+
if (now >= notOnOrAfter) {
|
|
345
|
+
return err(new AuthError('SAML assertion expired'));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Check audience restriction
|
|
349
|
+
const audience = xmlGetText(xml, 'Audience');
|
|
350
|
+
if (audience && audience !== this.config.spEntityId) {
|
|
351
|
+
return err(new AuthError(`SAML audience mismatch: expected ${this.config.spEntityId}, got ${audience}`));
|
|
352
|
+
}
|
|
353
|
+
return ok(undefined);
|
|
354
|
+
}
|
|
355
|
+
}
|