The server cannot process the request due to a client error. Please check the request and try again. If you’re the application owner check the logs for more information.
109 |├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app └── models │ └── granity │ └── relation_tuple.rb ├── bin ├── rails ├── rubocop └── setup ├── config └── routes.rb ├── db └── migrate │ └── 20250317000000_create_granity_relation_tuples.rb ├── granity.gemspec ├── lib ├── generators │ └── granity │ │ └── install │ │ ├── USAGE │ │ ├── install_generator.rb │ │ └── templates │ │ ├── create_granity_tables.rb │ │ └── initializer.rb ├── granity.rb ├── granity │ ├── authorization_engine.rb │ ├── configuration.rb │ ├── dependency_analyzer.rb │ ├── engine.rb │ ├── in_memory_cache.rb │ ├── permission.rb │ ├── permission_evaluator.rb │ ├── relation.rb │ ├── resource_type.rb │ ├── rules.rb │ ├── schema.rb │ └── version.rb └── tasks │ └── granity_tasks.rake └── spec ├── dummy ├── Rakefile ├── app │ ├── assets │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ └── application_controller.rb │ ├── models │ │ └── application_record.rb │ └── views │ │ ├── layouts │ │ └── application.html.erb │ │ └── pwa │ │ ├── manifest.json.erb │ │ └── service-worker.js ├── bin │ ├── dev │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── assets.rb │ │ ├── content_security_policy.rb │ │ ├── filter_parameter_logging.rb │ │ ├── granity.rb │ │ └── inflections.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ └── routes.rb ├── db │ └── schema.rb └── public │ ├── 400.html │ ├── 404.html │ ├── 406-unsupported-browser.html │ ├── 422.html │ ├── 500.html │ ├── icon.png │ └── icon.svg ├── factories └── examples.rb ├── granity ├── authorization_engine_spec.rb ├── permission_evaluator_spec.rb └── schema_spec.rb ├── integration └── scenarios │ ├── drive_authorization_spec.rb │ ├── github_authorization_spec.rb │ └── project_management_authorization_spec.rb ├── rails_helper.rb ├── spec_helper.rb └── support └── factory_bot.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: ruby-3.4.2 19 | bundler-cache: true 20 | 21 | - name: Lint code for consistent style 22 | run: bin/rubocop -f github 23 | 24 | test: 25 | runs-on: ubuntu-latest 26 | 27 | # services: 28 | # redis: 29 | # image: redis 30 | # ports: 31 | # - 6379:6379 32 | # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 33 | steps: 34 | - name: Install packages 35 | run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config google-chrome-stable 36 | 37 | - name: Checkout code 38 | uses: actions/checkout@v4 39 | 40 | - name: Set up Ruby 41 | uses: ruby/setup-ruby@v1 42 | with: 43 | ruby-version: ruby-3.4.2 44 | bundler-cache: true 45 | 46 | - name: Run tests 47 | env: 48 | RAILS_ENV: test 49 | # REDIS_URL: redis://localhost:6379/0 50 | run: bundle exec rspec --force-color 51 | 52 | - name: Keep screenshots from failed system tests 53 | uses: actions/upload-artifact@v4 54 | if: failure() 55 | with: 56 | name: screenshots 57 | path: ${{ github.workspace }}/tmp/screenshots 58 | if-no-files-found: ignore 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /doc/ 3 | /log/*.log 4 | /pkg/ 5 | /tmp/ 6 | /spec/dummy/db/*.sqlite3 7 | /spec/dummy/db/*.sqlite3-* 8 | /spec/dummy/log/*.log 9 | /spec/dummy/storage/ 10 | /spec/dummy/tmp/ 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | --format documentation -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Bundler/DuplicatedGem: 2 | Enabled: true 3 | Include: 4 | - "**/*.gemfile" 5 | - "**/Gemfile" 6 | - "**/gems.rb" 7 | 8 | Bundler/DuplicatedGroup: 9 | Enabled: false 10 | 11 | Bundler/GemComment: 12 | Enabled: false 13 | 14 | Bundler/GemFilename: 15 | Enabled: false 16 | 17 | Bundler/GemVersion: 18 | Enabled: false 19 | 20 | Bundler/InsecureProtocolSource: 21 | Enabled: true 22 | Include: 23 | - "**/*.gemfile" 24 | - "**/Gemfile" 25 | - "**/gems.rb" 26 | 27 | Bundler/OrderedGems: 28 | Enabled: false 29 | 30 | Gemspec/AddRuntimeDependency: 31 | Enabled: false 32 | 33 | Gemspec/DependencyVersion: 34 | Enabled: false 35 | 36 | Gemspec/DeprecatedAttributeAssignment: 37 | Enabled: true 38 | 39 | Gemspec/DevelopmentDependencies: 40 | Enabled: false 41 | 42 | Gemspec/DuplicatedAssignment: 43 | Enabled: true 44 | Include: 45 | - "**/*.gemspec" 46 | 47 | Gemspec/OrderedDependencies: 48 | Enabled: false 49 | 50 | Gemspec/RequireMFA: 51 | Enabled: false 52 | 53 | Gemspec/RequiredRubyVersion: 54 | Enabled: false 55 | 56 | Gemspec/RubyVersionGlobalsUsage: 57 | Enabled: false 58 | 59 | Layout/AccessModifierIndentation: 60 | Enabled: true 61 | EnforcedStyle: indent 62 | IndentationWidth: ~ 63 | 64 | Layout/ArgumentAlignment: 65 | Enabled: true 66 | EnforcedStyle: with_fixed_indentation 67 | 68 | Layout/ArrayAlignment: 69 | Enabled: true 70 | EnforcedStyle: with_fixed_indentation 71 | 72 | Layout/AssignmentIndentation: 73 | Enabled: true 74 | IndentationWidth: ~ 75 | 76 | Layout/BeginEndAlignment: 77 | Enabled: true 78 | EnforcedStyleAlignWith: start_of_line 79 | Severity: warning 80 | 81 | Layout/BlockAlignment: 82 | Enabled: true 83 | EnforcedStyleAlignWith: either 84 | 85 | Layout/BlockEndNewline: 86 | Enabled: true 87 | 88 | Layout/CaseIndentation: 89 | Enabled: true 90 | EnforcedStyle: end 91 | 92 | Layout/ClassStructure: 93 | Enabled: false 94 | 95 | Layout/ClosingHeredocIndentation: 96 | Enabled: true 97 | 98 | Layout/ClosingParenthesisIndentation: 99 | Enabled: true 100 | 101 | Layout/CommentIndentation: 102 | Enabled: true 103 | 104 | Layout/ConditionPosition: 105 | Enabled: true 106 | 107 | Layout/DefEndAlignment: 108 | Enabled: true 109 | EnforcedStyleAlignWith: start_of_line 110 | Severity: warning 111 | 112 | Layout/DotPosition: 113 | Enabled: true 114 | EnforcedStyle: leading 115 | 116 | Layout/ElseAlignment: 117 | Enabled: true 118 | 119 | Layout/EmptyComment: 120 | Enabled: true 121 | AllowBorderComment: true 122 | AllowMarginComment: true 123 | 124 | Layout/EmptyLineAfterGuardClause: 125 | Enabled: false 126 | 127 | Layout/EmptyLineAfterMagicComment: 128 | Enabled: true 129 | 130 | Layout/EmptyLineAfterMultilineCondition: 131 | Enabled: false 132 | 133 | Layout/EmptyLineBetweenDefs: 134 | Enabled: true 135 | AllowAdjacentOneLineDefs: false 136 | NumberOfEmptyLines: 1 137 | 138 | Layout/EmptyLines: 139 | Enabled: true 140 | 141 | Layout/EmptyLinesAroundAccessModifier: 142 | Enabled: true 143 | 144 | Layout/EmptyLinesAroundArguments: 145 | Enabled: true 146 | 147 | Layout/EmptyLinesAroundAttributeAccessor: 148 | Enabled: false 149 | 150 | Layout/EmptyLinesAroundBeginBody: 151 | Enabled: true 152 | 153 | Layout/EmptyLinesAroundBlockBody: 154 | Enabled: true 155 | EnforcedStyle: no_empty_lines 156 | 157 | Layout/EmptyLinesAroundClassBody: 158 | Enabled: true 159 | EnforcedStyle: no_empty_lines 160 | 161 | Layout/EmptyLinesAroundExceptionHandlingKeywords: 162 | Enabled: true 163 | 164 | Layout/EmptyLinesAroundMethodBody: 165 | Enabled: true 166 | 167 | Layout/EmptyLinesAroundModuleBody: 168 | Enabled: true 169 | EnforcedStyle: no_empty_lines 170 | 171 | Layout/EndAlignment: 172 | Enabled: true 173 | EnforcedStyleAlignWith: variable 174 | Severity: warning 175 | 176 | Layout/EndOfLine: 177 | Enabled: true 178 | EnforcedStyle: native 179 | 180 | Layout/ExtraSpacing: 181 | Enabled: true 182 | AllowForAlignment: false 183 | AllowBeforeTrailingComments: true 184 | ForceEqualSignAlignment: false 185 | 186 | Layout/FirstArgumentIndentation: 187 | Enabled: true 188 | EnforcedStyle: consistent 189 | IndentationWidth: ~ 190 | 191 | Layout/FirstArrayElementIndentation: 192 | Enabled: true 193 | EnforcedStyle: consistent 194 | IndentationWidth: ~ 195 | 196 | Layout/FirstArrayElementLineBreak: 197 | Enabled: false 198 | 199 | Layout/FirstHashElementIndentation: 200 | Enabled: true 201 | EnforcedStyle: consistent 202 | IndentationWidth: ~ 203 | 204 | Layout/FirstHashElementLineBreak: 205 | Enabled: false 206 | 207 | Layout/FirstMethodArgumentLineBreak: 208 | Enabled: false 209 | 210 | Layout/FirstMethodParameterLineBreak: 211 | Enabled: false 212 | 213 | Layout/FirstParameterIndentation: 214 | Enabled: false 215 | 216 | Layout/HashAlignment: 217 | Enabled: true 218 | EnforcedHashRocketStyle: key 219 | EnforcedColonStyle: key 220 | EnforcedLastArgumentHashStyle: always_inspect 221 | 222 | Layout/HeredocArgumentClosingParenthesis: 223 | Enabled: false 224 | 225 | Layout/HeredocIndentation: 226 | Enabled: true 227 | 228 | Layout/IndentationConsistency: 229 | Enabled: true 230 | EnforcedStyle: normal 231 | 232 | Layout/IndentationStyle: 233 | Enabled: true 234 | IndentationWidth: ~ 235 | 236 | Layout/IndentationWidth: 237 | Enabled: true 238 | Width: 2 239 | AllowedPatterns: [] 240 | 241 | Layout/InitialIndentation: 242 | Enabled: true 243 | 244 | Layout/LeadingCommentSpace: 245 | Enabled: true 246 | 247 | Layout/LeadingEmptyLines: 248 | Enabled: true 249 | 250 | Layout/LineContinuationLeadingSpace: 251 | Enabled: false 252 | 253 | Layout/LineContinuationSpacing: 254 | Enabled: true 255 | 256 | Layout/LineEndStringConcatenationIndentation: 257 | Enabled: false 258 | 259 | Layout/LineLength: 260 | Enabled: false 261 | 262 | Layout/MultilineArrayBraceLayout: 263 | Enabled: true 264 | EnforcedStyle: symmetrical 265 | 266 | Layout/MultilineArrayLineBreaks: 267 | Enabled: false 268 | 269 | Layout/MultilineAssignmentLayout: 270 | Enabled: false 271 | 272 | Layout/MultilineBlockLayout: 273 | Enabled: true 274 | 275 | Layout/MultilineHashBraceLayout: 276 | Enabled: true 277 | EnforcedStyle: symmetrical 278 | 279 | Layout/MultilineHashKeyLineBreaks: 280 | Enabled: false 281 | 282 | Layout/MultilineMethodArgumentLineBreaks: 283 | Enabled: false 284 | 285 | Layout/MultilineMethodCallBraceLayout: 286 | Enabled: true 287 | EnforcedStyle: symmetrical 288 | 289 | Layout/MultilineMethodCallIndentation: 290 | Enabled: true 291 | EnforcedStyle: indented 292 | IndentationWidth: ~ 293 | 294 | Layout/MultilineMethodDefinitionBraceLayout: 295 | Enabled: true 296 | EnforcedStyle: symmetrical 297 | 298 | Layout/MultilineMethodParameterLineBreaks: 299 | Enabled: false 300 | 301 | Layout/MultilineOperationIndentation: 302 | Enabled: true 303 | EnforcedStyle: indented 304 | IndentationWidth: ~ 305 | 306 | Layout/ParameterAlignment: 307 | Enabled: true 308 | EnforcedStyle: with_fixed_indentation 309 | IndentationWidth: ~ 310 | 311 | Layout/RedundantLineBreak: 312 | Enabled: false 313 | 314 | Layout/RescueEnsureAlignment: 315 | Enabled: true 316 | 317 | Layout/SingleLineBlockChain: 318 | Enabled: false 319 | 320 | Layout/SpaceAfterColon: 321 | Enabled: true 322 | 323 | Layout/SpaceAfterComma: 324 | Enabled: true 325 | 326 | Layout/SpaceAfterMethodName: 327 | Enabled: true 328 | 329 | Layout/SpaceAfterNot: 330 | Enabled: true 331 | 332 | Layout/SpaceAfterSemicolon: 333 | Enabled: true 334 | 335 | Layout/SpaceAroundBlockParameters: 336 | Enabled: true 337 | EnforcedStyleInsidePipes: no_space 338 | 339 | Layout/SpaceAroundEqualsInParameterDefault: 340 | Enabled: true 341 | EnforcedStyle: space 342 | 343 | Layout/SpaceAroundKeyword: 344 | Enabled: true 345 | 346 | Layout/SpaceAroundMethodCallOperator: 347 | Enabled: true 348 | 349 | Layout/SpaceAroundOperators: 350 | Enabled: true 351 | AllowForAlignment: true 352 | 353 | Layout/SpaceBeforeBlockBraces: 354 | Enabled: true 355 | EnforcedStyle: space 356 | EnforcedStyleForEmptyBraces: space 357 | 358 | Layout/SpaceBeforeBrackets: 359 | Enabled: false 360 | 361 | Layout/SpaceBeforeComma: 362 | Enabled: true 363 | 364 | Layout/SpaceBeforeComment: 365 | Enabled: true 366 | 367 | Layout/SpaceBeforeFirstArg: 368 | Enabled: true 369 | AllowForAlignment: true 370 | 371 | Layout/SpaceBeforeSemicolon: 372 | Enabled: true 373 | 374 | Layout/SpaceInLambdaLiteral: 375 | Enabled: true 376 | EnforcedStyle: require_no_space 377 | 378 | Layout/SpaceInsideArrayLiteralBrackets: 379 | Enabled: true 380 | EnforcedStyle: no_space 381 | EnforcedStyleForEmptyBrackets: no_space 382 | 383 | Layout/SpaceInsideArrayPercentLiteral: 384 | Enabled: true 385 | 386 | Layout/SpaceInsideBlockBraces: 387 | Enabled: true 388 | EnforcedStyle: space 389 | EnforcedStyleForEmptyBraces: no_space 390 | SpaceBeforeBlockParameters: true 391 | 392 | Layout/SpaceInsideHashLiteralBraces: 393 | Enabled: true 394 | EnforcedStyle: no_space 395 | EnforcedStyleForEmptyBraces: no_space 396 | 397 | Layout/SpaceInsideParens: 398 | Enabled: true 399 | EnforcedStyle: no_space 400 | 401 | Layout/SpaceInsidePercentLiteralDelimiters: 402 | Enabled: true 403 | 404 | Layout/SpaceInsideRangeLiteral: 405 | Enabled: true 406 | 407 | Layout/SpaceInsideReferenceBrackets: 408 | Enabled: true 409 | EnforcedStyle: no_space 410 | EnforcedStyleForEmptyBrackets: no_space 411 | 412 | Layout/SpaceInsideStringInterpolation: 413 | Enabled: true 414 | EnforcedStyle: no_space 415 | 416 | Layout/TrailingEmptyLines: 417 | Enabled: true 418 | EnforcedStyle: final_newline 419 | 420 | Layout/TrailingWhitespace: 421 | Enabled: true 422 | AllowInHeredoc: true 423 | 424 | Lint/AmbiguousAssignment: 425 | Enabled: true 426 | 427 | Lint/AmbiguousBlockAssociation: 428 | Enabled: false 429 | 430 | Lint/AmbiguousOperator: 431 | Enabled: true 432 | 433 | Lint/AmbiguousOperatorPrecedence: 434 | Enabled: false 435 | 436 | Lint/AmbiguousRange: 437 | Enabled: false 438 | 439 | Lint/AmbiguousRegexpLiteral: 440 | Enabled: true 441 | 442 | Lint/ArrayLiteralInRegexp: 443 | Enabled: true 444 | AutoCorrect: false 445 | 446 | Lint/AssignmentInCondition: 447 | Enabled: true 448 | AllowSafeAssignment: true 449 | # Intentionally disable autocorrect to force us to intentionally decide 450 | # whether assignment is intended as opposed to comparison 451 | AutoCorrect: false 452 | 453 | Lint/BigDecimalNew: 454 | Enabled: true 455 | 456 | Lint/BinaryOperatorWithIdenticalOperands: 457 | Enabled: true 458 | 459 | Lint/BooleanSymbol: 460 | Enabled: true 461 | 462 | Lint/CircularArgumentReference: 463 | Enabled: true 464 | 465 | Lint/ConstantDefinitionInBlock: 466 | Enabled: true 467 | 468 | Lint/ConstantOverwrittenInRescue: 469 | Enabled: true 470 | 471 | Lint/ConstantReassignment: 472 | Enabled: true 473 | 474 | Lint/ConstantResolution: 475 | Enabled: false 476 | 477 | Lint/CopDirectiveSyntax: 478 | Enabled: true 479 | 480 | Lint/Debugger: 481 | Enabled: true 482 | 483 | Lint/DeprecatedClassMethods: 484 | Enabled: true 485 | 486 | Lint/DeprecatedConstants: 487 | Enabled: true 488 | 489 | Lint/DeprecatedOpenSSLConstant: 490 | Enabled: true 491 | 492 | Lint/DisjunctiveAssignmentInConstructor: 493 | Enabled: false 494 | 495 | Lint/DuplicateBranch: 496 | Enabled: false 497 | 498 | Lint/DuplicateCaseCondition: 499 | Enabled: true 500 | 501 | Lint/DuplicateElsifCondition: 502 | Enabled: true 503 | 504 | Lint/DuplicateHashKey: 505 | Enabled: true 506 | 507 | Lint/DuplicateMagicComment: 508 | Enabled: true 509 | 510 | Lint/DuplicateMatchPattern: 511 | Enabled: false 512 | 513 | Lint/DuplicateMethods: 514 | Enabled: true 515 | 516 | Lint/DuplicateRegexpCharacterClassElement: 517 | Enabled: true 518 | 519 | Lint/DuplicateRequire: 520 | Enabled: true 521 | 522 | Lint/DuplicateRescueException: 523 | Enabled: true 524 | 525 | Lint/DuplicateSetElement: 526 | Enabled: false 527 | 528 | Lint/EachWithObjectArgument: 529 | Enabled: true 530 | 531 | Lint/ElseLayout: 532 | Enabled: true 533 | 534 | Lint/EmptyBlock: 535 | Enabled: false 536 | 537 | Lint/EmptyClass: 538 | Enabled: false 539 | 540 | Lint/EmptyConditionalBody: 541 | Enabled: false 542 | 543 | Lint/EmptyEnsure: 544 | Enabled: true 545 | 546 | Lint/EmptyExpression: 547 | Enabled: true 548 | 549 | Lint/EmptyFile: 550 | Enabled: false 551 | 552 | Lint/EmptyInPattern: 553 | Enabled: false 554 | 555 | Lint/EmptyInterpolation: 556 | Enabled: true 557 | 558 | Lint/EmptyWhen: 559 | Enabled: true 560 | AllowComments: true 561 | 562 | Lint/EnsureReturn: 563 | Enabled: true 564 | 565 | Lint/ErbNewArguments: 566 | Enabled: true 567 | 568 | Lint/FlipFlop: 569 | Enabled: true 570 | 571 | Lint/FloatComparison: 572 | Enabled: true 573 | 574 | Lint/FloatOutOfRange: 575 | Enabled: true 576 | 577 | Lint/FormatParameterMismatch: 578 | Enabled: true 579 | 580 | Lint/HashCompareByIdentity: 581 | Enabled: false 582 | 583 | Lint/HashNewWithKeywordArgumentsAsDefault: 584 | Enabled: true 585 | 586 | Lint/HeredocMethodCallPosition: 587 | Enabled: false 588 | 589 | Lint/IdentityComparison: 590 | Enabled: true 591 | 592 | Lint/ImplicitStringConcatenation: 593 | Enabled: true 594 | 595 | Lint/IncompatibleIoSelectWithFiberScheduler: 596 | Enabled: false 597 | 598 | Lint/IneffectiveAccessModifier: 599 | Enabled: true 600 | 601 | Lint/InheritException: 602 | Enabled: true 603 | EnforcedStyle: runtime_error 604 | 605 | Lint/InterpolationCheck: 606 | Enabled: true 607 | 608 | Lint/ItWithoutArgumentsInBlock: 609 | Enabled: true 610 | 611 | Lint/LambdaWithoutLiteralBlock: 612 | Enabled: false 613 | 614 | Lint/LiteralAsCondition: 615 | Enabled: true 616 | 617 | Lint/LiteralAssignmentInCondition: 618 | Enabled: true 619 | 620 | Lint/LiteralInInterpolation: 621 | Enabled: true 622 | 623 | Lint/Loop: 624 | Enabled: true 625 | 626 | Lint/MissingCopEnableDirective: 627 | Enabled: true 628 | MaximumRangeSize: .inf 629 | 630 | Lint/MissingSuper: 631 | Enabled: false 632 | 633 | Lint/MixedCaseRange: 634 | Enabled: true 635 | 636 | Lint/MixedRegexpCaptureTypes: 637 | Enabled: true 638 | 639 | Lint/MultipleComparison: 640 | Enabled: true 641 | 642 | Lint/NestedMethodDefinition: 643 | Enabled: true 644 | 645 | Lint/NestedPercentLiteral: 646 | Enabled: true 647 | 648 | Lint/NextWithoutAccumulator: 649 | Enabled: true 650 | 651 | Lint/NoReturnInBeginEndBlocks: 652 | Enabled: false 653 | 654 | Lint/NonAtomicFileOperation: 655 | Enabled: false 656 | 657 | Lint/NonDeterministicRequireOrder: 658 | Enabled: true 659 | 660 | Lint/NonLocalExitFromIterator: 661 | Enabled: true 662 | 663 | Lint/NumberConversion: 664 | Enabled: false 665 | 666 | Lint/NumberedParameterAssignment: 667 | Enabled: true 668 | 669 | Lint/NumericOperationWithConstantResult: 670 | Enabled: false 671 | 672 | Lint/OrAssignmentToConstant: 673 | Enabled: true 674 | 675 | Lint/OrderedMagicComments: 676 | Enabled: true 677 | 678 | Lint/OutOfRangeRegexpRef: 679 | Enabled: true 680 | 681 | Lint/ParenthesesAsGroupedExpression: 682 | Enabled: true 683 | 684 | Lint/PercentStringArray: 685 | Enabled: false 686 | 687 | Lint/PercentSymbolArray: 688 | Enabled: true 689 | 690 | Lint/RaiseException: 691 | Enabled: true 692 | 693 | Lint/RandOne: 694 | Enabled: true 695 | 696 | Lint/RedundantCopDisableDirective: 697 | Enabled: false 698 | 699 | Lint/RedundantCopEnableDirective: 700 | Enabled: false 701 | 702 | Lint/RedundantDirGlobSort: 703 | Enabled: false 704 | 705 | Lint/RedundantRegexpQuantifiers: 706 | Enabled: true 707 | 708 | Lint/RedundantRequireStatement: 709 | Enabled: true 710 | 711 | Lint/RedundantSafeNavigation: 712 | Enabled: false 713 | 714 | Lint/RedundantSplatExpansion: 715 | Enabled: true 716 | 717 | Lint/RedundantStringCoercion: 718 | Enabled: true 719 | 720 | Lint/RedundantTypeConversion: 721 | Enabled: true 722 | 723 | Lint/RedundantWithIndex: 724 | Enabled: true 725 | 726 | Lint/RedundantWithObject: 727 | Enabled: true 728 | 729 | Lint/RefinementImportMethods: 730 | Enabled: true 731 | 732 | Lint/RegexpAsCondition: 733 | Enabled: true 734 | 735 | Lint/RequireParentheses: 736 | Enabled: true 737 | 738 | Lint/RequireRangeParentheses: 739 | Enabled: true 740 | 741 | Lint/RequireRelativeSelfPath: 742 | Enabled: true 743 | 744 | Lint/RescueException: 745 | Enabled: true 746 | 747 | Lint/RescueType: 748 | Enabled: true 749 | 750 | Lint/ReturnInVoidContext: 751 | Enabled: true 752 | 753 | Lint/SafeNavigationChain: 754 | Enabled: true 755 | AllowedMethods: 756 | - present? 757 | - blank? 758 | - presence 759 | - try 760 | - try! 761 | 762 | Lint/SafeNavigationConsistency: 763 | Enabled: true 764 | AllowedMethods: 765 | - present? 766 | - blank? 767 | - presence 768 | - try 769 | - try! 770 | 771 | Lint/SafeNavigationWithEmpty: 772 | Enabled: true 773 | 774 | Lint/ScriptPermission: 775 | Enabled: false 776 | 777 | Lint/SelfAssignment: 778 | Enabled: true 779 | 780 | Lint/SendWithMixinArgument: 781 | Enabled: false 782 | 783 | Lint/ShadowedArgument: 784 | Enabled: true 785 | IgnoreImplicitReferences: false 786 | 787 | Lint/ShadowedException: 788 | Enabled: true 789 | 790 | Lint/ShadowingOuterLocalVariable: 791 | Enabled: false 792 | 793 | Lint/SharedMutableDefault: 794 | Enabled: true 795 | 796 | Lint/StructNewOverride: 797 | Enabled: false 798 | 799 | Lint/SuppressedException: 800 | Enabled: false 801 | 802 | Lint/SuppressedExceptionInNumberConversion: 803 | Enabled: true 804 | AutoCorrect: false 805 | 806 | Lint/SymbolConversion: 807 | Enabled: true 808 | 809 | Lint/Syntax: 810 | Enabled: true 811 | 812 | Lint/ToEnumArguments: 813 | Enabled: false 814 | 815 | Lint/ToJSON: 816 | Enabled: false 817 | 818 | Lint/TopLevelReturnWithArgument: 819 | Enabled: true 820 | 821 | Lint/TrailingCommaInAttributeDeclaration: 822 | Enabled: true 823 | 824 | Lint/TripleQuotes: 825 | Enabled: true 826 | 827 | Lint/UnderscorePrefixedVariableName: 828 | Enabled: true 829 | 830 | Lint/UnescapedBracketInRegexp: 831 | Enabled: false 832 | 833 | Lint/UnexpectedBlockArity: 834 | Enabled: false 835 | 836 | Lint/UnifiedInteger: 837 | Enabled: true 838 | 839 | Lint/UnmodifiedReduceAccumulator: 840 | Enabled: false 841 | 842 | Lint/UnreachableCode: 843 | Enabled: true 844 | 845 | Lint/UnreachableLoop: 846 | Enabled: false 847 | 848 | Lint/UnusedBlockArgument: 849 | Enabled: false 850 | 851 | Lint/UnusedMethodArgument: 852 | Enabled: false 853 | 854 | Lint/UriEscapeUnescape: 855 | Enabled: true 856 | 857 | Lint/UriRegexp: 858 | Enabled: true 859 | 860 | Lint/UselessAccessModifier: 861 | Enabled: false 862 | 863 | Lint/UselessAssignment: 864 | Enabled: true 865 | 866 | Lint/UselessConstantScoping: 867 | Enabled: true 868 | 869 | Lint/UselessDefined: 870 | Enabled: true 871 | 872 | Lint/UselessElseWithoutRescue: 873 | Enabled: false 874 | 875 | Lint/UselessMethodDefinition: 876 | Enabled: false 877 | 878 | Lint/UselessNumericOperation: 879 | Enabled: false 880 | 881 | Lint/UselessRescue: 882 | Enabled: true 883 | 884 | Lint/UselessRuby2Keywords: 885 | Enabled: true 886 | 887 | Lint/UselessSetterCall: 888 | Enabled: true 889 | 890 | Lint/UselessTimes: 891 | Enabled: true 892 | 893 | Lint/Void: 894 | Enabled: true 895 | CheckForMethodsWithNoSideEffects: false 896 | 897 | Metrics/AbcSize: 898 | Enabled: false 899 | 900 | Metrics/BlockLength: 901 | Enabled: false 902 | 903 | Metrics/BlockNesting: 904 | Enabled: false 905 | 906 | Metrics/ClassLength: 907 | Enabled: false 908 | 909 | Metrics/CollectionLiteralLength: 910 | Enabled: false 911 | 912 | Metrics/CyclomaticComplexity: 913 | Enabled: false 914 | 915 | Metrics/MethodLength: 916 | Enabled: false 917 | 918 | Metrics/ModuleLength: 919 | Enabled: false 920 | 921 | Metrics/ParameterLists: 922 | Enabled: false 923 | 924 | Metrics/PerceivedComplexity: 925 | Enabled: false 926 | 927 | Migration/DepartmentName: 928 | Enabled: true 929 | 930 | Naming/AccessorMethodName: 931 | Enabled: false 932 | 933 | Naming/AsciiIdentifiers: 934 | Enabled: false 935 | 936 | Naming/BinaryOperatorParameterName: 937 | Enabled: true 938 | 939 | Naming/BlockForwarding: 940 | Enabled: false 941 | 942 | Naming/BlockParameterName: 943 | Enabled: true 944 | MinNameLength: 1 945 | AllowNamesEndingInNumbers: true 946 | AllowedNames: [] 947 | ForbiddenNames: [] 948 | 949 | Naming/ClassAndModuleCamelCase: 950 | Enabled: true 951 | 952 | Naming/ConstantName: 953 | Enabled: true 954 | 955 | Naming/FileName: 956 | Enabled: false 957 | 958 | Naming/HeredocDelimiterCase: 959 | Enabled: true 960 | EnforcedStyle: uppercase 961 | 962 | Naming/HeredocDelimiterNaming: 963 | Enabled: false 964 | 965 | Naming/InclusiveLanguage: 966 | Enabled: false 967 | 968 | Naming/MemoizedInstanceVariableName: 969 | Enabled: false 970 | 971 | Naming/MethodName: 972 | Enabled: false 973 | 974 | Naming/MethodParameterName: 975 | Enabled: false 976 | 977 | Naming/PredicateName: 978 | Enabled: false 979 | 980 | Naming/RescuedExceptionsVariableName: 981 | Enabled: false 982 | 983 | Naming/VariableName: 984 | Enabled: true 985 | EnforcedStyle: snake_case 986 | 987 | Naming/VariableNumber: 988 | Enabled: false 989 | 990 | Security/CompoundHash: 991 | Enabled: true 992 | 993 | Security/Eval: 994 | Enabled: true 995 | 996 | Security/IoMethods: 997 | Enabled: false 998 | 999 | Security/JSONLoad: 1000 | Enabled: true 1001 | 1002 | Security/MarshalLoad: 1003 | Enabled: false 1004 | 1005 | Security/Open: 1006 | Enabled: true 1007 | 1008 | Security/YAMLLoad: 1009 | Enabled: true 1010 | 1011 | Style/AccessModifierDeclarations: 1012 | Enabled: false 1013 | 1014 | Style/AccessorGrouping: 1015 | Enabled: false 1016 | 1017 | Style/Alias: 1018 | Enabled: true 1019 | EnforcedStyle: prefer_alias_method 1020 | 1021 | Style/AmbiguousEndlessMethodDefinition: 1022 | Enabled: false 1023 | 1024 | Style/AndOr: 1025 | Enabled: true 1026 | 1027 | Style/ArgumentsForwarding: 1028 | Enabled: false 1029 | 1030 | Style/ArrayCoercion: 1031 | Enabled: false 1032 | 1033 | Style/ArrayFirstLast: 1034 | Enabled: false 1035 | 1036 | Style/ArrayIntersect: 1037 | Enabled: false 1038 | 1039 | Style/ArrayJoin: 1040 | Enabled: true 1041 | 1042 | Style/AsciiComments: 1043 | Enabled: false 1044 | 1045 | Style/Attr: 1046 | Enabled: true 1047 | 1048 | Style/AutoResourceCleanup: 1049 | Enabled: false 1050 | 1051 | Style/BarePercentLiterals: 1052 | Enabled: true 1053 | EnforcedStyle: bare_percent 1054 | 1055 | Style/BeginBlock: 1056 | Enabled: true 1057 | 1058 | Style/BisectedAttrAccessor: 1059 | Enabled: false 1060 | 1061 | Style/BitwisePredicate: 1062 | Enabled: false 1063 | 1064 | Style/BlockComments: 1065 | Enabled: true 1066 | 1067 | Style/BlockDelimiters: 1068 | Enabled: false 1069 | 1070 | Style/CaseEquality: 1071 | Enabled: false 1072 | 1073 | Style/CaseLikeIf: 1074 | Enabled: false 1075 | 1076 | Style/CharacterLiteral: 1077 | Enabled: true 1078 | 1079 | Style/ClassAndModuleChildren: 1080 | Enabled: false 1081 | 1082 | Style/ClassCheck: 1083 | Enabled: true 1084 | EnforcedStyle: is_a? 1085 | 1086 | Style/ClassEqualityComparison: 1087 | Enabled: true 1088 | 1089 | Style/ClassMethods: 1090 | Enabled: true 1091 | 1092 | Style/ClassMethodsDefinitions: 1093 | Enabled: false 1094 | 1095 | Style/ClassVars: 1096 | Enabled: false 1097 | 1098 | Style/CollectionCompact: 1099 | Enabled: false 1100 | 1101 | Style/CollectionMethods: 1102 | Enabled: false 1103 | 1104 | Style/ColonMethodCall: 1105 | Enabled: true 1106 | 1107 | Style/ColonMethodDefinition: 1108 | Enabled: true 1109 | 1110 | Style/CombinableDefined: 1111 | Enabled: false 1112 | 1113 | Style/CombinableLoops: 1114 | Enabled: false 1115 | 1116 | Style/CommandLiteral: 1117 | Enabled: true 1118 | EnforcedStyle: mixed 1119 | AllowInnerBackticks: false 1120 | 1121 | Style/CommentAnnotation: 1122 | Enabled: false 1123 | 1124 | Style/CommentedKeyword: 1125 | Enabled: false 1126 | 1127 | Style/ComparableClamp: 1128 | Enabled: true 1129 | 1130 | Style/ConcatArrayLiterals: 1131 | Enabled: false 1132 | 1133 | Style/ConditionalAssignment: 1134 | Enabled: true 1135 | EnforcedStyle: assign_to_condition 1136 | SingleLineConditionsOnly: true 1137 | IncludeTernaryExpressions: true 1138 | 1139 | Style/ConstantVisibility: 1140 | Enabled: false 1141 | 1142 | Style/Copyright: 1143 | Enabled: false 1144 | 1145 | Style/DataInheritance: 1146 | Enabled: false 1147 | 1148 | Style/DateTime: 1149 | Enabled: false 1150 | 1151 | Style/DefWithParentheses: 1152 | Enabled: true 1153 | 1154 | Style/DigChain: 1155 | Enabled: true 1156 | 1157 | Style/Dir: 1158 | Enabled: true 1159 | 1160 | Style/DirEmpty: 1161 | Enabled: true 1162 | 1163 | Style/DisableCopsWithinSourceCodeDirective: 1164 | Enabled: false 1165 | 1166 | Style/DocumentDynamicEvalDefinition: 1167 | Enabled: false 1168 | 1169 | Style/Documentation: 1170 | Enabled: false 1171 | 1172 | Style/DocumentationMethod: 1173 | Enabled: false 1174 | 1175 | Style/DoubleCopDisableDirective: 1176 | Enabled: false 1177 | 1178 | Style/DoubleNegation: 1179 | Enabled: false 1180 | 1181 | Style/EachForSimpleLoop: 1182 | Enabled: true 1183 | 1184 | Style/EachWithObject: 1185 | Enabled: true 1186 | 1187 | Style/EmptyBlockParameter: 1188 | Enabled: true 1189 | 1190 | Style/EmptyCaseCondition: 1191 | Enabled: true 1192 | 1193 | Style/EmptyElse: 1194 | Enabled: true 1195 | AllowComments: true 1196 | EnforcedStyle: both 1197 | 1198 | Style/EmptyHeredoc: 1199 | Enabled: false 1200 | 1201 | Style/EmptyLambdaParameter: 1202 | Enabled: true 1203 | 1204 | Style/EmptyLiteral: 1205 | Enabled: true 1206 | 1207 | Style/EmptyMethod: 1208 | Enabled: true 1209 | EnforcedStyle: expanded 1210 | 1211 | Style/Encoding: 1212 | Enabled: true 1213 | 1214 | Style/EndBlock: 1215 | Enabled: true 1216 | 1217 | Style/EndlessMethod: 1218 | Enabled: false 1219 | 1220 | Style/EnvHome: 1221 | Enabled: false 1222 | 1223 | Style/EvalWithLocation: 1224 | Enabled: true 1225 | 1226 | Style/EvenOdd: 1227 | Enabled: false 1228 | 1229 | Style/ExactRegexpMatch: 1230 | Enabled: true 1231 | Style/ExpandPathArguments: 1232 | Enabled: false 1233 | 1234 | Style/ExplicitBlockArgument: 1235 | Enabled: false 1236 | 1237 | Style/ExponentialNotation: 1238 | Enabled: false 1239 | 1240 | Style/FetchEnvVar: 1241 | Enabled: false 1242 | 1243 | Style/FileEmpty: 1244 | Enabled: false 1245 | 1246 | Style/FileNull: 1247 | Enabled: true 1248 | 1249 | Style/FileRead: 1250 | Enabled: true 1251 | 1252 | Style/FileTouch: 1253 | Enabled: false 1254 | 1255 | Style/FileWrite: 1256 | Enabled: true 1257 | 1258 | Style/FloatDivision: 1259 | Enabled: false 1260 | 1261 | Style/For: 1262 | Enabled: true 1263 | EnforcedStyle: each 1264 | 1265 | Style/FormatString: 1266 | Enabled: false 1267 | 1268 | Style/FormatStringToken: 1269 | Enabled: false 1270 | 1271 | Style/FrozenStringLiteralComment: 1272 | Enabled: false 1273 | 1274 | Style/GlobalStdStream: 1275 | Enabled: true 1276 | 1277 | Style/GlobalVars: 1278 | Enabled: true 1279 | AllowedVariables: [] 1280 | 1281 | Style/GuardClause: 1282 | Enabled: false 1283 | 1284 | Style/HashAsLastArrayItem: 1285 | Enabled: false 1286 | 1287 | Style/HashConversion: 1288 | Enabled: true 1289 | 1290 | Style/HashEachMethods: 1291 | Enabled: false 1292 | 1293 | Style/HashExcept: 1294 | Enabled: true 1295 | 1296 | Style/HashLikeCase: 1297 | Enabled: false 1298 | 1299 | Style/HashSlice: 1300 | Enabled: true 1301 | 1302 | Style/HashSyntax: 1303 | Enabled: true 1304 | EnforcedStyle: ruby19_no_mixed_keys 1305 | EnforcedShorthandSyntax: either 1306 | 1307 | Style/HashTransformKeys: 1308 | Enabled: false 1309 | 1310 | Style/HashTransformValues: 1311 | Enabled: false 1312 | 1313 | Style/IdenticalConditionalBranches: 1314 | Enabled: true 1315 | 1316 | Style/IfInsideElse: 1317 | Enabled: true 1318 | 1319 | Style/IfUnlessModifier: 1320 | Enabled: false 1321 | 1322 | Style/IfUnlessModifierOfIfUnless: 1323 | Enabled: true 1324 | 1325 | Style/IfWithBooleanLiteralBranches: 1326 | Enabled: true 1327 | 1328 | Style/IfWithSemicolon: 1329 | Enabled: true 1330 | 1331 | Style/ImplicitRuntimeError: 1332 | Enabled: false 1333 | 1334 | Style/InPatternThen: 1335 | Enabled: false 1336 | 1337 | Style/InfiniteLoop: 1338 | Enabled: true 1339 | 1340 | Style/InlineComment: 1341 | Enabled: false 1342 | 1343 | Style/InverseMethods: 1344 | Enabled: false 1345 | 1346 | Style/InvertibleUnlessCondition: 1347 | Enabled: false 1348 | 1349 | Style/IpAddresses: 1350 | Enabled: false 1351 | 1352 | Style/ItAssignment: 1353 | Enabled: true 1354 | 1355 | Style/KeywordArgumentsMerging: 1356 | Enabled: false 1357 | 1358 | Style/KeywordParametersOrder: 1359 | Enabled: true 1360 | 1361 | Style/Lambda: 1362 | Enabled: false 1363 | 1364 | Style/LambdaCall: 1365 | Enabled: true 1366 | EnforcedStyle: call 1367 | 1368 | Style/LineEndConcatenation: 1369 | Enabled: true 1370 | 1371 | Style/MagicCommentFormat: 1372 | Enabled: false 1373 | 1374 | Style/MapCompactWithConditionalBlock: 1375 | Enabled: true 1376 | 1377 | Style/MapIntoArray: 1378 | Enabled: false 1379 | 1380 | Style/MapToHash: 1381 | Enabled: false 1382 | 1383 | Style/MapToSet: 1384 | Enabled: false 1385 | 1386 | Style/MethodCallWithArgsParentheses: 1387 | Enabled: false 1388 | 1389 | Style/MethodCallWithoutArgsParentheses: 1390 | Enabled: true 1391 | AllowedMethods: [] 1392 | 1393 | Style/MethodCalledOnDoEndBlock: 1394 | Enabled: false 1395 | 1396 | Style/MethodDefParentheses: 1397 | Enabled: false 1398 | 1399 | Style/MinMax: 1400 | Enabled: false 1401 | 1402 | Style/MinMaxComparison: 1403 | Enabled: false 1404 | 1405 | Style/MissingElse: 1406 | Enabled: false 1407 | 1408 | Style/MissingRespondToMissing: 1409 | Enabled: true 1410 | 1411 | Style/MixinGrouping: 1412 | Enabled: true 1413 | EnforcedStyle: separated 1414 | 1415 | Style/MixinUsage: 1416 | Enabled: true 1417 | 1418 | Style/ModuleFunction: 1419 | Enabled: false 1420 | 1421 | Style/MultilineBlockChain: 1422 | Enabled: false 1423 | 1424 | Style/MultilineIfModifier: 1425 | Enabled: true 1426 | 1427 | Style/MultilineIfThen: 1428 | Enabled: true 1429 | 1430 | Style/MultilineInPatternThen: 1431 | Enabled: false 1432 | 1433 | Style/MultilineMemoization: 1434 | Enabled: true 1435 | EnforcedStyle: keyword 1436 | 1437 | Style/MultilineMethodSignature: 1438 | Enabled: false 1439 | 1440 | Style/MultilineTernaryOperator: 1441 | Enabled: false 1442 | 1443 | Style/MultilineWhenThen: 1444 | Enabled: true 1445 | 1446 | Style/MultipleComparison: 1447 | Enabled: false 1448 | 1449 | Style/MutableConstant: 1450 | Enabled: false 1451 | 1452 | Style/NegatedIf: 1453 | Enabled: false 1454 | 1455 | Style/NegatedIfElseCondition: 1456 | Enabled: false 1457 | 1458 | Style/NegatedUnless: 1459 | Enabled: false 1460 | 1461 | Style/NegatedWhile: 1462 | Enabled: true 1463 | 1464 | Style/NestedFileDirname: 1465 | Enabled: true 1466 | 1467 | Style/NestedModifier: 1468 | Enabled: true 1469 | 1470 | Style/NestedParenthesizedCalls: 1471 | Enabled: true 1472 | AllowedMethods: 1473 | - be 1474 | - be_a 1475 | - be_an 1476 | - be_between 1477 | - be_falsey 1478 | - be_kind_of 1479 | - be_instance_of 1480 | - be_truthy 1481 | - be_within 1482 | - eq 1483 | - eql 1484 | - end_with 1485 | - include 1486 | - match 1487 | - raise_error 1488 | - respond_to 1489 | - start_with 1490 | 1491 | Style/NestedTernaryOperator: 1492 | Enabled: true 1493 | 1494 | Style/Next: 1495 | Enabled: false 1496 | 1497 | Style/NilComparison: 1498 | Enabled: true 1499 | EnforcedStyle: predicate 1500 | 1501 | Style/NilLambda: 1502 | Enabled: true 1503 | 1504 | Style/NonNilCheck: 1505 | Enabled: true 1506 | IncludeSemanticChanges: false 1507 | 1508 | Style/Not: 1509 | Enabled: true 1510 | 1511 | Style/NumberedParameters: 1512 | Enabled: false 1513 | 1514 | Style/NumberedParametersLimit: 1515 | Enabled: false 1516 | 1517 | Style/NumericLiteralPrefix: 1518 | Enabled: true 1519 | EnforcedOctalStyle: zero_with_o 1520 | 1521 | Style/NumericLiterals: 1522 | Enabled: false 1523 | 1524 | Style/NumericPredicate: 1525 | Enabled: false 1526 | 1527 | Style/ObjectThen: 1528 | Enabled: false 1529 | 1530 | Style/OneLineConditional: 1531 | Enabled: true 1532 | 1533 | Style/OpenStructUse: 1534 | Enabled: false 1535 | 1536 | Style/OperatorMethodCall: 1537 | Enabled: false 1538 | 1539 | Style/OptionHash: 1540 | Enabled: false 1541 | 1542 | Style/OptionalArguments: 1543 | Enabled: true 1544 | 1545 | Style/OptionalBooleanParameter: 1546 | Enabled: false 1547 | 1548 | Style/OrAssignment: 1549 | Enabled: true 1550 | 1551 | Style/ParallelAssignment: 1552 | Enabled: false 1553 | 1554 | Style/ParenthesesAroundCondition: 1555 | Enabled: true 1556 | AllowSafeAssignment: true 1557 | AllowInMultilineConditions: false 1558 | 1559 | Style/PercentLiteralDelimiters: 1560 | Enabled: true 1561 | PreferredDelimiters: 1562 | default: () 1563 | "%i": "[]" 1564 | "%I": "[]" 1565 | "%r": "{}" 1566 | "%w": "[]" 1567 | "%W": "[]" 1568 | 1569 | Style/PercentQLiterals: 1570 | Enabled: false 1571 | 1572 | Style/PerlBackrefs: 1573 | Enabled: false 1574 | 1575 | Style/PreferredHashMethods: 1576 | Enabled: false 1577 | 1578 | Style/Proc: 1579 | Enabled: true 1580 | 1581 | Style/QuotedSymbols: 1582 | Enabled: true 1583 | EnforcedStyle: same_as_string_literals 1584 | 1585 | Style/RaiseArgs: 1586 | Enabled: false 1587 | 1588 | Style/RandomWithOffset: 1589 | Enabled: true 1590 | 1591 | Style/RedundantArgument: 1592 | Enabled: false 1593 | 1594 | Style/RedundantArrayConstructor: 1595 | Enabled: true 1596 | 1597 | Style/RedundantAssignment: 1598 | Enabled: true 1599 | 1600 | Style/RedundantBegin: 1601 | Enabled: true 1602 | 1603 | Style/RedundantCapitalW: 1604 | Enabled: false 1605 | 1606 | Style/RedundantCondition: 1607 | Enabled: true 1608 | 1609 | Style/RedundantConditional: 1610 | Enabled: true 1611 | 1612 | Style/RedundantConstantBase: 1613 | Enabled: false 1614 | 1615 | Style/RedundantCurrentDirectoryInPath: 1616 | Enabled: true 1617 | 1618 | Style/RedundantDoubleSplatHashBraces: 1619 | Enabled: true 1620 | 1621 | Style/RedundantEach: 1622 | Enabled: false 1623 | 1624 | Style/RedundantException: 1625 | Enabled: true 1626 | 1627 | Style/RedundantFetchBlock: 1628 | Enabled: false 1629 | 1630 | Style/RedundantFileExtensionInRequire: 1631 | Enabled: true 1632 | 1633 | Style/RedundantFilterChain: 1634 | Enabled: false 1635 | 1636 | Style/RedundantFormat: 1637 | Enabled: true 1638 | 1639 | Style/RedundantFreeze: 1640 | Enabled: true 1641 | 1642 | Style/RedundantHeredocDelimiterQuotes: 1643 | Enabled: true 1644 | 1645 | Style/RedundantInitialize: 1646 | Enabled: false 1647 | 1648 | Style/RedundantInterpolation: 1649 | Enabled: true 1650 | 1651 | Style/RedundantInterpolationUnfreeze: 1652 | Enabled: true 1653 | 1654 | Style/RedundantLineContinuation: 1655 | Enabled: true 1656 | 1657 | Style/RedundantParentheses: 1658 | Enabled: true 1659 | 1660 | Style/RedundantPercentQ: 1661 | Enabled: true 1662 | 1663 | Style/RedundantRegexpArgument: 1664 | Enabled: true 1665 | 1666 | Style/RedundantRegexpCharacterClass: 1667 | Enabled: true 1668 | 1669 | Style/RedundantRegexpConstructor: 1670 | Enabled: true 1671 | 1672 | Style/RedundantRegexpEscape: 1673 | Enabled: true 1674 | 1675 | Style/RedundantReturn: 1676 | Enabled: true 1677 | AllowMultipleReturnValues: false 1678 | 1679 | Style/RedundantSelf: 1680 | Enabled: true 1681 | 1682 | Style/RedundantSelfAssignment: 1683 | Enabled: false 1684 | 1685 | Style/RedundantSelfAssignmentBranch: 1686 | Enabled: false 1687 | 1688 | Style/RedundantSort: 1689 | Enabled: true 1690 | 1691 | Style/RedundantSortBy: 1692 | Enabled: true 1693 | 1694 | Style/RedundantStringEscape: 1695 | Enabled: true 1696 | 1697 | Style/RegexpLiteral: 1698 | Enabled: false 1699 | 1700 | Style/RequireOrder: 1701 | Enabled: false 1702 | 1703 | Style/RescueModifier: 1704 | Enabled: true 1705 | 1706 | Style/RescueStandardError: 1707 | Enabled: true 1708 | EnforcedStyle: implicit 1709 | 1710 | Style/ReturnNil: 1711 | Enabled: false 1712 | 1713 | Style/ReturnNilInPredicateMethodDefinition: 1714 | Enabled: false 1715 | 1716 | Style/SafeNavigation: 1717 | Enabled: true 1718 | ConvertCodeThatCanStartToReturnNil: false 1719 | AllowedMethods: 1720 | - present? 1721 | - blank? 1722 | - presence 1723 | - try 1724 | - try! 1725 | 1726 | Style/SafeNavigationChainLength: 1727 | Enabled: false 1728 | 1729 | Style/Sample: 1730 | Enabled: true 1731 | 1732 | Style/SelectByRegexp: 1733 | Enabled: false 1734 | 1735 | Style/SelfAssignment: 1736 | Enabled: true 1737 | 1738 | Style/Semicolon: 1739 | Enabled: true 1740 | AllowAsExpressionSeparator: false 1741 | 1742 | Style/Send: 1743 | Enabled: false 1744 | 1745 | Style/SendWithLiteralMethodName: 1746 | Enabled: false 1747 | 1748 | Style/SignalException: 1749 | Enabled: false 1750 | 1751 | Style/SingleArgumentDig: 1752 | Enabled: false 1753 | 1754 | Style/SingleLineBlockParams: 1755 | Enabled: false 1756 | 1757 | Style/SingleLineDoEndBlock: 1758 | Enabled: false 1759 | 1760 | Style/SingleLineMethods: 1761 | Enabled: true 1762 | AllowIfMethodIsEmpty: false 1763 | 1764 | Style/SlicingWithRange: 1765 | Enabled: true 1766 | 1767 | Style/SoleNestedConditional: 1768 | Enabled: false 1769 | 1770 | Style/SpecialGlobalVars: 1771 | Enabled: false 1772 | 1773 | Style/StabbyLambdaParentheses: 1774 | Enabled: true 1775 | EnforcedStyle: require_parentheses 1776 | 1777 | Style/StaticClass: 1778 | Enabled: false 1779 | 1780 | Style/StderrPuts: 1781 | Enabled: true 1782 | 1783 | Style/StringChars: 1784 | Enabled: true 1785 | 1786 | Style/StringConcatenation: 1787 | Enabled: false 1788 | 1789 | Style/StringHashKeys: 1790 | Enabled: false 1791 | 1792 | Style/StringLiterals: 1793 | Enabled: true 1794 | EnforcedStyle: double_quotes 1795 | ConsistentQuotesInMultiline: false 1796 | 1797 | Style/StringLiteralsInInterpolation: 1798 | Enabled: true 1799 | EnforcedStyle: double_quotes 1800 | 1801 | Style/StringMethods: 1802 | Enabled: false 1803 | 1804 | Style/Strip: 1805 | Enabled: true 1806 | 1807 | Style/StructInheritance: 1808 | Enabled: false 1809 | 1810 | Style/SuperArguments: 1811 | Enabled: true 1812 | 1813 | Style/SuperWithArgsParentheses: 1814 | Enabled: true 1815 | 1816 | Style/SwapValues: 1817 | Enabled: false 1818 | 1819 | Style/SymbolArray: 1820 | Enabled: false 1821 | 1822 | Style/SymbolLiteral: 1823 | Enabled: true 1824 | 1825 | Style/SymbolProc: 1826 | Enabled: false 1827 | 1828 | Style/TernaryParentheses: 1829 | Enabled: true 1830 | EnforcedStyle: require_parentheses_when_complex 1831 | AllowSafeAssignment: true 1832 | 1833 | Style/TopLevelMethodDefinition: 1834 | Enabled: false 1835 | 1836 | Style/TrailingBodyOnClass: 1837 | Enabled: true 1838 | 1839 | Style/TrailingBodyOnMethodDefinition: 1840 | Enabled: true 1841 | 1842 | Style/TrailingBodyOnModule: 1843 | Enabled: true 1844 | 1845 | Style/TrailingCommaInArguments: 1846 | Enabled: true 1847 | EnforcedStyleForMultiline: no_comma 1848 | 1849 | Style/TrailingCommaInArrayLiteral: 1850 | Enabled: true 1851 | EnforcedStyleForMultiline: no_comma 1852 | 1853 | Style/TrailingCommaInBlockArgs: 1854 | Enabled: true 1855 | 1856 | Style/TrailingCommaInHashLiteral: 1857 | Enabled: true 1858 | EnforcedStyleForMultiline: no_comma 1859 | 1860 | Style/TrailingMethodEndStatement: 1861 | Enabled: true 1862 | 1863 | Style/TrailingUnderscoreVariable: 1864 | Enabled: false 1865 | 1866 | Style/TrivialAccessors: 1867 | Enabled: true 1868 | ExactNameMatch: true 1869 | AllowPredicates: true 1870 | AllowDSLWriters: false 1871 | IgnoreClassMethods: true 1872 | AllowedMethods: 1873 | - to_ary 1874 | - to_a 1875 | - to_c 1876 | - to_enum 1877 | - to_h 1878 | - to_hash 1879 | - to_i 1880 | - to_int 1881 | - to_io 1882 | - to_open 1883 | - to_path 1884 | - to_proc 1885 | - to_r 1886 | - to_regexp 1887 | - to_str 1888 | - to_s 1889 | - to_sym 1890 | 1891 | Style/UnlessElse: 1892 | Enabled: true 1893 | 1894 | Style/UnlessLogicalOperators: 1895 | Enabled: true 1896 | EnforcedStyle: forbid_mixed_logical_operators 1897 | 1898 | Style/UnpackFirst: 1899 | Enabled: true 1900 | 1901 | Style/VariableInterpolation: 1902 | Enabled: true 1903 | 1904 | Style/WhenThen: 1905 | Enabled: true 1906 | 1907 | Style/WhileUntilDo: 1908 | Enabled: true 1909 | 1910 | Style/WhileUntilModifier: 1911 | Enabled: false 1912 | 1913 | Style/WordArray: 1914 | Enabled: false 1915 | 1916 | Style/YAMLFileRead: 1917 | Enabled: true 1918 | 1919 | Style/YodaCondition: 1920 | Enabled: true 1921 | EnforcedStyle: forbid_for_all_comparison_operators 1922 | 1923 | Style/YodaExpression: 1924 | Enabled: false 1925 | 1926 | Style/ZeroLengthPredicate: 1927 | Enabled: false 1928 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Specify your gem's dependencies in granity.gemspec. 5 | gemspec 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | granity (0.1.3) 5 | activerecord (>= 7.1) 6 | railties (>= 7.1) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actionpack (8.0.2) 12 | actionview (= 8.0.2) 13 | activesupport (= 8.0.2) 14 | nokogiri (>= 1.8.5) 15 | rack (>= 2.2.4) 16 | rack-session (>= 1.0.1) 17 | rack-test (>= 0.6.3) 18 | rails-dom-testing (~> 2.2) 19 | rails-html-sanitizer (~> 1.6) 20 | useragent (~> 0.16) 21 | actionview (8.0.2) 22 | activesupport (= 8.0.2) 23 | builder (~> 3.1) 24 | erubi (~> 1.11) 25 | rails-dom-testing (~> 2.2) 26 | rails-html-sanitizer (~> 1.6) 27 | activemodel (8.0.2) 28 | activesupport (= 8.0.2) 29 | activerecord (8.0.2) 30 | activemodel (= 8.0.2) 31 | activesupport (= 8.0.2) 32 | timeout (>= 0.4.0) 33 | activesupport (8.0.2) 34 | base64 35 | benchmark (>= 0.3) 36 | bigdecimal 37 | concurrent-ruby (~> 1.0, >= 1.3.1) 38 | connection_pool (>= 2.2.5) 39 | drb 40 | i18n (>= 1.6, < 2) 41 | logger (>= 1.4.2) 42 | minitest (>= 5.1) 43 | securerandom (>= 0.3) 44 | tzinfo (~> 2.0, >= 2.0.5) 45 | uri (>= 0.13.1) 46 | ast (2.4.3) 47 | base64 (0.2.0) 48 | benchmark (0.4.0) 49 | bigdecimal (3.1.9) 50 | builder (3.3.0) 51 | concurrent-ruby (1.3.5) 52 | connection_pool (2.5.0) 53 | crass (1.0.6) 54 | database_cleaner-active_record (2.2.0) 55 | activerecord (>= 5.a) 56 | database_cleaner-core (~> 2.0.0) 57 | database_cleaner-core (2.0.1) 58 | date (3.4.1) 59 | diff-lcs (1.6.0) 60 | drb (2.2.1) 61 | erubi (1.13.1) 62 | factory_bot (6.5.1) 63 | activesupport (>= 6.1.0) 64 | factory_bot_rails (6.4.4) 65 | factory_bot (~> 6.5) 66 | railties (>= 5.0.0) 67 | i18n (1.14.7) 68 | concurrent-ruby (~> 1.0) 69 | io-console (0.8.0) 70 | irb (1.15.1) 71 | pp (>= 0.6.0) 72 | rdoc (>= 4.0.0) 73 | reline (>= 0.4.2) 74 | json (2.10.2) 75 | language_server-protocol (3.17.0.4) 76 | lint_roller (1.1.0) 77 | logger (1.6.6) 78 | loofah (2.24.0) 79 | crass (~> 1.0.2) 80 | nokogiri (>= 1.12.0) 81 | minitest (5.25.5) 82 | nokogiri (1.18.4-aarch64-linux-gnu) 83 | racc (~> 1.4) 84 | nokogiri (1.18.4-aarch64-linux-musl) 85 | racc (~> 1.4) 86 | nokogiri (1.18.4-arm-linux-gnu) 87 | racc (~> 1.4) 88 | nokogiri (1.18.4-arm-linux-musl) 89 | racc (~> 1.4) 90 | nokogiri (1.18.4-arm64-darwin) 91 | racc (~> 1.4) 92 | nokogiri (1.18.4-x86_64-darwin) 93 | racc (~> 1.4) 94 | nokogiri (1.18.4-x86_64-linux-gnu) 95 | racc (~> 1.4) 96 | nokogiri (1.18.4-x86_64-linux-musl) 97 | racc (~> 1.4) 98 | parallel (1.27.0) 99 | parser (3.3.8.0) 100 | ast (~> 2.4.1) 101 | racc 102 | pp (0.6.2) 103 | prettyprint 104 | prettyprint (0.2.0) 105 | prism (1.4.0) 106 | psych (5.2.3) 107 | date 108 | stringio 109 | racc (1.8.1) 110 | rack (3.1.12) 111 | rack-session (2.1.0) 112 | base64 (>= 0.1.0) 113 | rack (>= 3.0.0) 114 | rack-test (2.2.0) 115 | rack (>= 1.3) 116 | rackup (2.2.1) 117 | rack (>= 3) 118 | rails-dom-testing (2.2.0) 119 | activesupport (>= 5.0.0) 120 | minitest 121 | nokogiri (>= 1.6) 122 | rails-html-sanitizer (1.6.2) 123 | loofah (~> 2.21) 124 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 125 | railties (8.0.2) 126 | actionpack (= 8.0.2) 127 | activesupport (= 8.0.2) 128 | irb (~> 1.13) 129 | rackup (>= 1.0.0) 130 | rake (>= 12.2) 131 | thor (~> 1.0, >= 1.2.2) 132 | zeitwerk (~> 2.6) 133 | rainbow (3.1.1) 134 | rake (13.2.1) 135 | rdoc (6.12.0) 136 | psych (>= 4.0.0) 137 | regexp_parser (2.10.0) 138 | reline (0.6.0) 139 | io-console (~> 0.5) 140 | rspec-core (3.13.3) 141 | rspec-support (~> 3.13.0) 142 | rspec-expectations (3.13.3) 143 | diff-lcs (>= 1.2.0, < 2.0) 144 | rspec-support (~> 3.13.0) 145 | rspec-mocks (3.13.2) 146 | diff-lcs (>= 1.2.0, < 2.0) 147 | rspec-support (~> 3.13.0) 148 | rspec-rails (7.1.1) 149 | actionpack (>= 7.0) 150 | activesupport (>= 7.0) 151 | railties (>= 7.0) 152 | rspec-core (~> 3.13) 153 | rspec-expectations (~> 3.13) 154 | rspec-mocks (~> 3.13) 155 | rspec-support (~> 3.13) 156 | rspec-support (3.13.2) 157 | rubocop (1.75.3) 158 | json (~> 2.3) 159 | language_server-protocol (~> 3.17.0.2) 160 | lint_roller (~> 1.1.0) 161 | parallel (~> 1.10) 162 | parser (>= 3.3.0.2) 163 | rainbow (>= 2.2.2, < 4.0) 164 | regexp_parser (>= 2.9.3, < 3.0) 165 | rubocop-ast (>= 1.44.0, < 2.0) 166 | ruby-progressbar (~> 1.7) 167 | unicode-display_width (>= 2.4.0, < 4.0) 168 | rubocop-ast (1.44.1) 169 | parser (>= 3.3.7.2) 170 | prism (~> 1.4) 171 | ruby-progressbar (1.13.0) 172 | securerandom (0.4.1) 173 | sqlite3 (2.6.0-aarch64-linux-gnu) 174 | sqlite3 (2.6.0-aarch64-linux-musl) 175 | sqlite3 (2.6.0-arm-linux-gnu) 176 | sqlite3 (2.6.0-arm-linux-musl) 177 | sqlite3 (2.6.0-arm64-darwin) 178 | sqlite3 (2.6.0-x86_64-darwin) 179 | sqlite3 (2.6.0-x86_64-linux-gnu) 180 | sqlite3 (2.6.0-x86_64-linux-musl) 181 | stringio (3.1.5) 182 | thor (1.3.2) 183 | timeout (0.4.3) 184 | tzinfo (2.0.6) 185 | concurrent-ruby (~> 1.0) 186 | unicode-display_width (3.1.4) 187 | unicode-emoji (~> 4.0, >= 4.0.4) 188 | unicode-emoji (4.0.4) 189 | uri (1.0.3) 190 | useragent (0.16.11) 191 | zeitwerk (2.7.2) 192 | 193 | PLATFORMS 194 | aarch64-linux-gnu 195 | aarch64-linux-musl 196 | arm-linux-gnu 197 | arm-linux-musl 198 | arm64-darwin 199 | x86_64-darwin 200 | x86_64-linux-gnu 201 | x86_64-linux-musl 202 | 203 | DEPENDENCIES 204 | database_cleaner-active_record 205 | factory_bot_rails 206 | granity! 207 | rspec-rails 208 | rubocop 209 | sqlite3 210 | 211 | BUNDLED WITH 212 | 2.6.5 213 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright TODO: Write your name 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Granity 2 | 3 | Granity is a fine-grained authorization engine for Ruby on Rails applications. It provides a flexible DSL for defining authorization rules and efficient permission checking. 4 | 5 | ## Installation 6 | 7 | Add this gem to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'granity' 11 | ``` 12 | 13 | Then execute: 14 | 15 | ```bash 16 | $ bundle install 17 | ``` 18 | 19 | Run the migrations: 20 | 21 | ```bash 22 | rails granity:install:migrations 23 | rails db:migrate 24 | ``` 25 | 26 | ## Configuration 27 | 28 | Create an initializer for Granity: 29 | 30 | ```ruby 31 | # config/initializers/granity.rb 32 | Granity.configure do |config| 33 | config.cache_provider = Rails.cache # Optional: Uses Rails.cache if provided 34 | config.cache_ttl = 10.minutes 35 | config.max_cache_size = 10_000 36 | config.enable_tracing = !Rails.env.production? 37 | config.max_traversal_depth = 10 38 | end 39 | ``` 40 | 41 | ## Defining Authorization Schema 42 | 43 | Use the Granity DSL to define your authorization schema: 44 | 45 | ```ruby 46 | # config/initializers/granity.rb 47 | Granity.define do 48 | resource_type :user do 49 | # User schema 50 | end 51 | 52 | resource_type :document do 53 | relation :owner, type: :user 54 | relation :viewer, type: :user 55 | relation :team, type: :team 56 | 57 | permission :view do 58 | include_any do 59 | include_relation :owner 60 | include_relation :viewer 61 | include_relation :admin from :team 62 | end 63 | end 64 | 65 | permission :edit do 66 | include_relation :owner 67 | end 68 | end 69 | 70 | resource_type :team do 71 | relation :member, type: :user 72 | relation :admin, type: :user 73 | end 74 | end 75 | ``` 76 | 77 | ## Usage 78 | 79 | ### Checking Permissions 80 | 81 | ```ruby 82 | # Check if a user has permission on a resource 83 | if Granity.check_permission( 84 | subject_type: 'user', 85 | subject_id: current_user.id, 86 | permission: 'view', 87 | resource_type: 'document', 88 | resource_id: document.id 89 | ) 90 | # User can view the document 91 | end 92 | ``` 93 | 94 | ### Creating Relations 95 | 96 | ```ruby 97 | # Grant a user owner access to a document 98 | Granity.create_relation( 99 | object_type: 'document', 100 | object_id: document.id, 101 | relation: 'owner', 102 | subject_type: 'user', 103 | subject_id: user.id 104 | ) 105 | ``` 106 | 107 | ### Finding Subjects 108 | 109 | ```ruby 110 | # Find all users who can view a document 111 | viewers = Granity.find_subjects( 112 | resource_type: 'document', 113 | resource_id: document.id, 114 | permission: 'view' 115 | ) 116 | ``` 117 | 118 | ### Integration with Rails Controllers 119 | 120 | ```ruby 121 | class ApplicationController < ActionController::Base 122 | def authorize!(resource, permission) 123 | unless Granity.check_permission( 124 | subject_type: 'user', 125 | subject_id: current_user.id, 126 | permission: permission, 127 | resource_type: resource.model_name.singular, 128 | resource_id: resource.id 129 | ) 130 | raise Unauthorized, "Not authorized to #{permission} this #{resource.model_name.human}" 131 | end 132 | end 133 | end 134 | 135 | class DocumentsController < ApplicationController 136 | def show 137 | @document = Document.find(params[:id]) 138 | authorize!(@document, :view) 139 | # ... 140 | end 141 | 142 | def update 143 | @document = Document.find(params[:id]) 144 | authorize!(@document, :edit) 145 | # ... 146 | end 147 | end 148 | ``` 149 | 150 | ## DSL Reference 151 | 152 | ### Resource Types 153 | 154 | Define the entities in your authorization model: 155 | 156 | ```ruby 157 | resource_type :document do 158 | # Resource definition 159 | end 160 | ``` 161 | 162 | ### Relations 163 | 164 | Define relationships between resources: 165 | 166 | ```ruby 167 | relation :owner, type: :user 168 | relation :parent_folder, type: :folder 169 | ``` 170 | 171 | ### Permissions 172 | 173 | Define access rules with Boolean logic: 174 | 175 | ```ruby 176 | permission :view do 177 | include_any do 178 | include_relation :owner 179 | include_relation :viewer 180 | include_relation :editor 181 | end 182 | end 183 | ``` 184 | 185 | ### Boolean Logic 186 | 187 | Combine relations with `include_any` (OR) and `include_all` (AND): 188 | 189 | ```ruby 190 | permission :publish do 191 | include_all do 192 | include_relation :editor 193 | 194 | include_any do 195 | include_relation :approved 196 | include_relation :admin from :team 197 | end 198 | end 199 | end 200 | ``` 201 | 202 | ### Permission Composition 203 | 204 | Reuse and compose permissions: 205 | 206 | ```ruby 207 | permission :manage do 208 | include_permission :view 209 | include_permission :edit 210 | include_relation :owner 211 | end 212 | ``` 213 | 214 | ### Relation Traversal 215 | 216 | Follow paths through related resources: 217 | 218 | ```ruby 219 | include_relation :member from :team 220 | ``` 221 | 222 | ## License 223 | 224 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 225 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__) 4 | load "rails/tasks/engine.rake" 5 | 6 | load "rails/tasks/statistics.rake" 7 | 8 | require "bundler/gem_tasks" 9 | -------------------------------------------------------------------------------- /app/models/granity/relation_tuple.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | class RelationTuple < ActiveRecord::Base 3 | self.table_name = "granity_relation_tuples" 4 | 5 | validates :object_type, :object_id, :relation, :subject_type, :subject_id, presence: true 6 | 7 | # Useful scopes for querying 8 | scope :for_object, ->(type, id) { where(object_type: type, object_id: id) } 9 | scope :for_subject, ->(type, id) { where(subject_type: type, subject_id: id) } 10 | scope :with_relation, ->(relation) { where(relation: relation) } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path("..", __dir__) 6 | ENGINE_PATH = File.expand_path("../lib/granity/engine", __dir__) 7 | APP_PATH = File.expand_path("../spec/dummy/config/application", __dir__) 8 | 9 | # Set up gems listed in the Gemfile. 10 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 11 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 12 | 13 | require "rails" 14 | # Pick the frameworks you want: 15 | require "active_model/railtie" 16 | # require "active_job/railtie" 17 | require "active_record/railtie" 18 | # require "active_storage/engine" 19 | # require "action_controller/railtie" 20 | # require "action_mailer/railtie" 21 | # require "action_mailbox/engine" 22 | # require "action_text/engine" 23 | # require "action_view/railtie" 24 | # require "action_cable/engine" 25 | # require "rails/test_unit/railtie" 26 | require "rails/engine/commands" 27 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | cd "$(dirname "${BASH_SOURCE[0]}")" 4 | 5 | bundle 6 | 7 | echo "Creating databases..." 8 | 9 | rails db:reset -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Granity::Engine.routes.draw do 2 | end 3 | -------------------------------------------------------------------------------- /db/migrate/20250317000000_create_granity_relation_tuples.rb: -------------------------------------------------------------------------------- 1 | class CreateGranityRelationTuples < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :granity_relation_tuples do |t| 4 | t.string :object_type, null: false 5 | t.string :object_id, null: false 6 | t.string :relation, null: false 7 | t.string :subject_type, null: false 8 | t.string :subject_id, null: false 9 | 10 | t.timestamps 11 | end 12 | 13 | add_index :granity_relation_tuples, [:object_type, :object_id, :relation], 14 | name: "index_granity_tuples_on_object" 15 | add_index :granity_relation_tuples, [:subject_type, :subject_id], 16 | name: "index_granity_tuples_on_subject" 17 | add_index :granity_relation_tuples, [:object_type, :object_id, :relation, :subject_type, :subject_id], 18 | unique: true, name: "index_granity_tuples_unique" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /granity.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/granity/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "granity" 5 | spec.version = Granity::VERSION 6 | spec.authors = ["Yatish Mehta"] 7 | spec.email = ["yatish27@users.noreply.github.com"] 8 | spec.homepage = "https://github.com/yatish27/granity" 9 | spec.summary = "Fine-grained authorization for Ruby on Rails" 10 | spec.description = "Granity is a flexible, caching-friendly authorization engine that provides fine-grained access control for Ruby on Rails applications" 11 | spec.license = "MIT" 12 | spec.required_ruby_version = ">= 3.1" 13 | 14 | spec.metadata["homepage_uri"] = spec.homepage 15 | spec.metadata["documentation_uri"] = "https://github.com/yatish27/granity/blob/main/README.md" 16 | spec.metadata["changelog_uri"] = "https://github.com/yatish27/granity/blob/main/CHANGELOG.md" 17 | 18 | spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] 19 | 20 | rails_version = ">= 7.1" 21 | spec.add_dependency "activerecord", rails_version 22 | spec.add_dependency "railties", rails_version 23 | 24 | spec.add_development_dependency "sqlite3" 25 | spec.add_development_dependency "rspec-rails" 26 | spec.add_development_dependency "factory_bot_rails" 27 | spec.add_development_dependency "database_cleaner-active_record" 28 | spec.add_development_dependency "rubocop" 29 | end 30 | -------------------------------------------------------------------------------- /lib/generators/granity/install/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | The granity:install generator creates the necessary migration file for 3 | the Granity authorization engine and adds a default initializer. 4 | 5 | Example: 6 | bin/rails generate granity:install 7 | 8 | This will create: 9 | db/migrate/TIMESTAMP_create_granity_tables.rb 10 | config/initializers/granity.rb -------------------------------------------------------------------------------- /lib/generators/granity/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | source_root File.expand_path("templates", __dir__) 5 | 6 | def create_migration 7 | timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S") 8 | copy_file "create_granity_tables.rb", "db/migrate/#{timestamp}_create_granity_tables.rb" 9 | end 10 | 11 | def create_initializer 12 | copy_file "initializer.rb", "config/initializers/granity.rb" 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/granity/install/templates/create_granity_tables.rb: -------------------------------------------------------------------------------- 1 | class CreateGranityTables < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :granity_relation_tuples do |t| 4 | t.string :object_type, null: false 5 | t.string :object_id, null: false 6 | t.string :relation, null: false 7 | t.string :subject_type, null: false 8 | t.string :subject_id, null: false 9 | 10 | t.timestamps 11 | end 12 | 13 | add_index :granity_relation_tuples, [:object_type, :object_id, :relation], 14 | name: "index_granity_tuples_on_object" 15 | add_index :granity_relation_tuples, [:subject_type, :subject_id], 16 | name: "index_granity_tuples_on_subject" 17 | add_index :granity_relation_tuples, [:object_type, :object_id, :relation, :subject_type, :subject_id], 18 | unique: true, name: "index_granity_tuples_unique" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/generators/granity/install/templates/initializer.rb: -------------------------------------------------------------------------------- 1 | Granity.configure do |config| 2 | # Configure cache provider (defaults to nil) 3 | # config.cache_provider = Rails.cache 4 | 5 | # Configure cache TTL (defaults to 10 minutes) 6 | # config.cache_ttl = 10.minutes 7 | 8 | # Configure max cache size (defaults to 10,000) 9 | # config.max_cache_size = 10_000 10 | 11 | # Enable tracing (defaults to true in non-production) 12 | # config.enable_tracing = !Rails.env.production? 13 | 14 | # Configure max traversal depth (defaults to 10) 15 | # config.max_traversal_depth = 10 16 | end 17 | 18 | # Define your authorization schema 19 | # Granity.define do 20 | # # Add your resource types, relations, and permissions here 21 | # end 22 | -------------------------------------------------------------------------------- /lib/granity.rb: -------------------------------------------------------------------------------- 1 | require "granity/version" 2 | require "granity/configuration" 3 | require "granity/schema" 4 | require "granity/resource_type" 5 | require "granity/relation" 6 | require "granity/permission" 7 | require "granity/rules" 8 | require "granity/in_memory_cache" 9 | require "granity/dependency_analyzer" 10 | require "granity/permission_evaluator" 11 | require "granity/authorization_engine" 12 | # Rails engine is loaded at the end 13 | require "granity/engine" if defined?(Rails) 14 | 15 | module Granity 16 | class Error < StandardError; end 17 | 18 | class << self 19 | def configuration 20 | @configuration ||= Configuration.new 21 | end 22 | 23 | # Entry point for DSL schema definition 24 | def define(&block) 25 | Schema.define(&block) 26 | end 27 | 28 | # Configuration setup 29 | def configure 30 | yield(configuration) if block_given? 31 | configuration 32 | end 33 | 34 | # Public API to check permissions 35 | def check_permission(subject_type:, subject_id:, permission:, resource_type:, resource_id:) 36 | AuthorizationEngine.check_permission( 37 | subject_type: subject_type, 38 | subject_id: subject_id, 39 | permission: permission, 40 | resource_type: resource_type, 41 | resource_id: resource_id 42 | ) 43 | end 44 | 45 | # Public API to find subjects with a permission 46 | def find_subjects(resource_type:, resource_id:, permission:) 47 | AuthorizationEngine.find_subjects( 48 | resource_type: resource_type, 49 | resource_id: resource_id, 50 | permission: permission 51 | ) 52 | end 53 | 54 | # Public API to create relation tuples 55 | def create_relation(object_type:, object_id:, relation:, subject_type:, subject_id:) 56 | AuthorizationEngine.create_relation( 57 | object_type: object_type, 58 | object_id: object_id, 59 | relation: relation, 60 | subject_type: subject_type, 61 | subject_id: subject_id 62 | ) 63 | end 64 | 65 | # Public API to delete relation tuples 66 | def delete_relation(object_type:, object_id:, relation:, subject_type:, subject_id:) 67 | AuthorizationEngine.delete_relation( 68 | object_type: object_type, 69 | object_id: object_id, 70 | relation: relation, 71 | subject_type: subject_type, 72 | subject_id: subject_id 73 | ) 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/granity/authorization_engine.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | class AuthorizationEngine 3 | class << self 4 | def check_permission(subject_type:, subject_id:, permission:, resource_type:, resource_id:) 5 | cache_key = "granity:permission:#{subject_type}:#{subject_id}:#{permission}:#{resource_type}:#{resource_id}" 6 | 7 | # Try fetching from cache first 8 | cached_result = cache.read(cache_key) 9 | if cached_result 10 | trace("CACHE HIT: #{cache_key} -> #{cached_result}") 11 | return cached_result 12 | end 13 | 14 | trace("CACHE MISS: #{cache_key}") 15 | 16 | # Generate dependencies for this permission check 17 | dependencies = DependencyAnalyzer.analyze_permission_check( 18 | subject_type: subject_type, 19 | subject_id: subject_id, 20 | permission: permission, 21 | resource_type: resource_type, 22 | resource_id: resource_id 23 | ) 24 | 25 | # Check the permission 26 | result = PermissionEvaluator.evaluate( 27 | subject_type: subject_type, 28 | subject_id: subject_id, 29 | permission: permission, 30 | resource_type: resource_type, 31 | resource_id: resource_id 32 | ) 33 | 34 | # Store in cache with all dependency keys 35 | cache.write(cache_key, result, dependencies: dependencies) 36 | trace("CACHE WRITE: #{cache_key} -> #{result} with dependencies: #{dependencies}") 37 | 38 | result 39 | end 40 | 41 | def find_subjects(resource_type:, resource_id:, permission:) 42 | cache_key = "granity:subjects:#{permission}:#{resource_type}:#{resource_id}" 43 | 44 | # Try fetching from cache first 45 | cached_result = cache.read(cache_key) 46 | if cached_result 47 | trace("CACHE HIT: #{cache_key}") 48 | return cached_result 49 | end 50 | 51 | trace("CACHE MISS: #{cache_key}") 52 | 53 | # Generate dependencies for this subjects query 54 | dependencies = DependencyAnalyzer.analyze_find_subjects( 55 | resource_type: resource_type, 56 | resource_id: resource_id, 57 | permission: permission 58 | ) 59 | 60 | # Get the subjects 61 | subjects = PermissionEvaluator.find_subjects( 62 | resource_type: resource_type, 63 | resource_id: resource_id, 64 | permission: permission 65 | ) 66 | 67 | # Store in cache with all dependency keys 68 | cache.write(cache_key, subjects, dependencies: dependencies) 69 | trace("CACHE WRITE: #{cache_key} with dependencies: #{dependencies}") 70 | 71 | subjects 72 | end 73 | 74 | def create_relation(object_type:, object_id:, relation:, subject_type:, subject_id:) 75 | # Create the relation tuple in the database 76 | tuple = Granity::RelationTuple.create!( 77 | object_type: object_type, 78 | object_id: object_id, 79 | relation: relation, 80 | subject_type: subject_type, 81 | subject_id: subject_id 82 | ) 83 | 84 | # Invalidate cache entries that depend on this relation 85 | invalidate_cache_for_relation(object_type, object_id, relation) 86 | 87 | tuple 88 | rescue ActiveRecord::RecordNotUnique 89 | # If the relation already exists, just return it 90 | Granity::RelationTuple.find_by( 91 | object_type: object_type, 92 | object_id: object_id, 93 | relation: relation, 94 | subject_type: subject_type, 95 | subject_id: subject_id 96 | ) 97 | end 98 | 99 | def delete_relation(object_type:, object_id:, relation:, subject_type:, subject_id:) 100 | tuple = Granity::RelationTuple.find_by( 101 | object_type: object_type, 102 | object_id: object_id, 103 | relation: relation, 104 | subject_type: subject_type, 105 | subject_id: subject_id 106 | ) 107 | 108 | return false unless tuple 109 | 110 | tuple.destroy 111 | 112 | # Invalidate cache entries that depend on this relation 113 | invalidate_cache_for_relation(object_type, object_id, relation) 114 | 115 | true 116 | end 117 | 118 | def reset_cache 119 | cache.clear 120 | end 121 | 122 | private 123 | 124 | def cache 125 | @cache ||= begin 126 | config = Granity.configuration 127 | config.cache_provider || Granity::InMemoryCache.new( 128 | max_size: config.max_cache_size, 129 | ttl: config.cache_ttl 130 | ) 131 | end 132 | end 133 | 134 | def invalidate_cache_for_relation(object_type, object_id, relation) 135 | # This is a simplified approach. In a real implementation, we would use 136 | # a more sophisticated approach to track which cache keys depend on which 137 | # relations, possibly using Redis sets or a similar mechanism. 138 | 139 | # For now, we take a conservative approach and invalidate any cache entry 140 | # that might depend on this relation being changed 141 | dependency_key = "granity:relation:#{object_type}:#{object_id}:#{relation}" 142 | cache.invalidate_dependencies([dependency_key]) 143 | 144 | trace("CACHE INVALIDATE for dependency: #{dependency_key}") 145 | end 146 | 147 | def trace(message) 148 | return unless Granity.configuration.enable_tracing 149 | 150 | # In a real implementation, this would use a proper logging system 151 | puts "[Granity] #{message}" 152 | end 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/granity/configuration.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | # Configuration class to hold Granity settings 3 | class Configuration 4 | attr_accessor :cache_provider 5 | attr_accessor :cache_ttl 6 | attr_accessor :max_cache_size 7 | attr_accessor :enable_tracing 8 | attr_accessor :max_traversal_depth 9 | 10 | def initialize 11 | @cache_provider = nil 12 | @cache_ttl = 10.minutes 13 | @max_cache_size = 10_000 14 | @enable_tracing = !defined?(Rails) || !Rails.env.production? 15 | @max_traversal_depth = 10 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/granity/dependency_analyzer.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | # Analyzes dependencies between relations and permissions for proper cache invalidation 3 | class DependencyAnalyzer 4 | class << self 5 | # Analyze dependencies for a permission check 6 | # Returns an array of dependencies for cache invalidation 7 | def analyze_permission_check(subject_type:, subject_id:, permission:, resource_type:, resource_id:) 8 | deps = [] 9 | 10 | # Basic direct dependencies 11 | deps << "granity:subject:#{subject_type}:#{subject_id}" 12 | deps << "granity:resource:#{resource_type}:#{resource_id}" 13 | 14 | # Get schema dependencies for this permission 15 | schema_dependencies = analyze_permission_schema(resource_type, permission) 16 | deps.concat(schema_dependencies) 17 | 18 | deps 19 | end 20 | 21 | # Analyze dependencies for finding subjects with a permission 22 | def analyze_find_subjects(resource_type:, resource_id:, permission:) 23 | deps = [] 24 | 25 | # Basic resource dependency 26 | deps << "granity:resource:#{resource_type}:#{resource_id}" 27 | 28 | # Get schema dependencies for this permission 29 | schema_dependencies = analyze_permission_schema(resource_type, permission) 30 | deps.concat(schema_dependencies) 31 | 32 | deps 33 | end 34 | 35 | private 36 | 37 | # Analyze schema dependencies for a permission 38 | def analyze_permission_schema(resource_type, permission) 39 | deps = [] 40 | schema = Granity::Schema.current 41 | 42 | # Add dependency on the permission definition itself 43 | deps << "granity:schema:#{resource_type}:permission:#{permission}" 44 | 45 | # Get the resource type from schema 46 | resource_type_def = schema.resource_types[resource_type.to_sym] 47 | return deps unless resource_type_def 48 | 49 | # Get the permission definition 50 | permission_def = resource_type_def.permissions[permission.to_sym] 51 | return deps unless permission_def 52 | 53 | # Track visited permissions to prevent cycles 54 | visited = Set.new(["#{resource_type}:#{permission}"]) 55 | 56 | # Add all relations that this permission depends on 57 | relations = extract_relations_from_permission(permission_def, [], visited) 58 | relations.each do |relation| 59 | deps << "granity:relation:#{resource_type}:#{relation}" 60 | end 61 | 62 | deps 63 | end 64 | 65 | # Extract all relations used in a permission definition 66 | def extract_relations_from_permission(rule_or_permission, relations = [], visited = Set.new) 67 | # Handle different types of input 68 | if rule_or_permission.is_a?(Granity::Permission) 69 | # It's a Permission object with rules 70 | rule_or_permission.rules.each do |rule| 71 | extract_relations_from_rule(rule, relations, visited, rule_or_permission.resource_type) 72 | end 73 | else 74 | # It's a rule object directly 75 | extract_relations_from_rule(rule_or_permission, relations, visited, nil) 76 | end 77 | 78 | relations.uniq 79 | end 80 | 81 | # Process a single rule to extract relations 82 | def extract_relations_from_rule(rule, relations = [], visited = Set.new, resource_type = nil) 83 | if rule.is_a?(Granity::Rules::Relation) 84 | # Direct relation - add to results 85 | relations << rule.relation 86 | elsif rule.is_a?(Granity::Rules::Any) || rule.is_a?(Granity::Rules::All) 87 | # Container rule - process each subrule 88 | rule.rules.each do |subrule| 89 | extract_relations_from_rule(subrule, relations, visited, resource_type) 90 | end 91 | elsif rule.is_a?(Granity::Rules::Permission) 92 | # Referenced permission - get from schema and process 93 | # Skip if we've already visited this permission to prevent cycles 94 | permission_key = "#{resource_type}:#{rule.permission}" 95 | return relations if visited.include?(permission_key) 96 | 97 | # Mark as visited to prevent cycles 98 | visited.add(permission_key) 99 | 100 | # Get referenced permission from schema (if possible) 101 | schema = Granity::Schema.current 102 | 103 | if resource_type && schema.resource_types[resource_type.to_sym] 104 | # If we know the resource type, look for the permission there 105 | resource_type_def = schema.resource_types[resource_type.to_sym] 106 | if resource_type_def.permissions.has_key?(rule.permission) 107 | referenced_permission = resource_type_def.permissions[rule.permission] 108 | extract_relations_from_permission(referenced_permission, relations, visited) 109 | end 110 | else 111 | # If resource type is unknown, look in all resource types 112 | resource_types = schema.resource_types.values 113 | resource_types.each do |rt| 114 | if rt.permissions.has_key?(rule.permission) 115 | referenced_permission = rt.permissions[rule.permission] 116 | extract_relations_from_permission(referenced_permission, relations, visited) 117 | break 118 | end 119 | end 120 | end 121 | end 122 | 123 | relations 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/granity/engine.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | class Engine < ::Rails::Engine 3 | isolate_namespace Granity 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/granity/in_memory_cache.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | # Simple in-memory cache with dependency tracking and TTL 3 | class InMemoryCache 4 | def initialize(max_size: 10_000, ttl: 600) 5 | @data = {} 6 | @dependencies = {} 7 | @max_size = max_size 8 | @default_ttl = ttl 9 | @mutex = Mutex.new 10 | end 11 | 12 | # Read a value from cache 13 | def read(key) 14 | @mutex.synchronize do 15 | entry = @data[key] 16 | return nil unless entry 17 | 18 | # Check if entry is expired 19 | if entry[:expires_at] && entry[:expires_at] < Time.now 20 | @data.delete(key) 21 | return nil 22 | end 23 | 24 | entry[:value] 25 | end 26 | end 27 | 28 | # Write a value to cache with dependencies 29 | def write(key, value, dependencies: [], ttl: nil) 30 | @mutex.synchronize do 31 | # Set expiration time if ttl provided 32 | expires_at = if ttl 33 | Time.now + ttl 34 | else 35 | (@default_ttl ? Time.now + @default_ttl : nil) 36 | end 37 | 38 | # Store the value 39 | @data[key] = { 40 | value: value, 41 | expires_at: expires_at 42 | } 43 | 44 | # Register dependencies 45 | dependencies.each do |dependency| 46 | @dependencies[dependency] ||= [] 47 | @dependencies[dependency] << key unless @dependencies[dependency].include?(key) 48 | end 49 | 50 | # Enforce max size by removing oldest entries if needed 51 | if @data.size > @max_size 52 | # Simple LRU - just remove oldest N/4 entries 53 | keys_to_remove = @data.keys.take(@max_size / 4) 54 | keys_to_remove.each { |k| @data.delete(k) } 55 | end 56 | 57 | value 58 | end 59 | end 60 | 61 | # Invalidate cache entries by dependency key 62 | def invalidate_dependencies(dependency_keys) 63 | @mutex.synchronize do 64 | keys_to_invalidate = [] 65 | 66 | dependency_keys.each do |dep_key| 67 | # Get all cache keys dependent on this key 68 | if @dependencies[dep_key] 69 | keys_to_invalidate.concat(@dependencies[dep_key]) 70 | @dependencies.delete(dep_key) 71 | end 72 | end 73 | 74 | # Remove the invalidated entries 75 | keys_to_invalidate.uniq.each do |key| 76 | @data.delete(key) 77 | end 78 | 79 | keys_to_invalidate.size 80 | end 81 | end 82 | 83 | # Clear the entire cache 84 | def clear 85 | @mutex.synchronize do 86 | @data.clear 87 | @dependencies.clear 88 | end 89 | end 90 | 91 | # Get current cache size 92 | def size 93 | @mutex.synchronize { @data.size } 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/granity/permission.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | # Definition of a permission in the authorization schema 3 | class Permission 4 | attr_reader :name, :resource_type, :description, :rules 5 | 6 | def initialize(name:, resource_type:, description: nil) 7 | @name = name 8 | @resource_type = resource_type 9 | @description = description 10 | @rules = [] 11 | end 12 | 13 | # Include a relation in the permission 14 | def include_relation(relation, from: nil) 15 | @rules << Rules::Relation.new(relation: relation, from: from) 16 | end 17 | 18 | # Include another permission in this permission 19 | def include_permission(permission, from: nil) 20 | @rules << Rules::Permission.new(permission: permission, from: from) 21 | end 22 | 23 | # Define a set of rules where ANY must match 24 | def include_any(&block) 25 | rule = Rules::Any.new 26 | rule.instance_eval(&block) 27 | @rules << rule 28 | end 29 | 30 | # Define a set of rules where ALL must match 31 | def include_all(&block) 32 | rule = Rules::All.new 33 | rule.instance_eval(&block) 34 | @rules << rule 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/granity/permission_evaluator.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | # Evaluates permissions based on the defined schema and relation tuples 3 | class PermissionEvaluator 4 | class << self 5 | # Evaluate if a subject has a permission on a resource 6 | def evaluate(subject_type:, subject_id:, permission:, resource_type:, resource_id:) 7 | # Get the schema definition 8 | schema = Granity::Schema.current 9 | resource_type_def = schema.resource_types[resource_type.to_sym] 10 | 11 | # If resource type doesn't exist, deny permission 12 | return false unless resource_type_def 13 | 14 | # Get the permission definition 15 | permission_def = resource_type_def.permissions[permission.to_sym] 16 | 17 | # If permission doesn't exist, deny permission 18 | return false unless permission_def 19 | 20 | # Evaluate the permission rules 21 | evaluate_rules( 22 | rules: permission_def.rules, 23 | subject_type: subject_type, 24 | subject_id: subject_id, 25 | resource_type: resource_type, 26 | resource_id: resource_id 27 | ) 28 | end 29 | 30 | # Find subjects with a permission on a resource 31 | def find_subjects(resource_type:, resource_id:, permission:) 32 | # Get the schema definition 33 | schema = Granity::Schema.current 34 | resource_type_def = schema.resource_types[resource_type.to_sym] 35 | 36 | # If resource type doesn't exist, return empty array 37 | return [] unless resource_type_def 38 | 39 | # Get the permission definition 40 | permission_def = resource_type_def.permissions[permission.to_sym] 41 | 42 | # If permission doesn't exist, return empty array 43 | return [] unless permission_def 44 | 45 | # Get all relation tuples for this resource 46 | collect_subjects_for_permission( 47 | rules: permission_def.rules, 48 | resource_type: resource_type, 49 | resource_id: resource_id 50 | ) 51 | end 52 | 53 | private 54 | 55 | # Evaluate rules recursively 56 | def evaluate_rules(rules:, subject_type:, subject_id:, resource_type:, resource_id:, depth: 0) 57 | # Prevent infinite recursion 58 | max_depth = Granity.configuration.max_traversal_depth 59 | if depth > max_depth 60 | raise "Maximum permission evaluation depth (#{max_depth}) exceeded" 61 | end 62 | 63 | rules.each do |rule| 64 | case rule 65 | when Granity::Rules::Relation 66 | # Check if relation tuple exists, handling the "from" case 67 | if rule.from 68 | # Relation traversal is needed 69 | if check_relation_traversal( 70 | subject_type: subject_type, 71 | subject_id: subject_id, 72 | relation: rule.relation, 73 | from_relation: rule.from, 74 | object_type: resource_type, 75 | object_id: resource_id 76 | ) 77 | return true 78 | end 79 | elsif check_relation( 80 | subject_type: subject_type, 81 | subject_id: subject_id, 82 | relation: rule.relation, 83 | object_type: resource_type, 84 | object_id: resource_id 85 | ) 86 | # Direct relation check 87 | return true 88 | end 89 | when Granity::Rules::Permission 90 | # Check referenced permission, handling "from" case 91 | if rule.from 92 | # Permission check with traversal - check permission on the "from" related objects 93 | if check_permission_traversal( 94 | subject_type: subject_type, 95 | subject_id: subject_id, 96 | permission: rule.permission, 97 | from_relation: rule.from, 98 | object_type: resource_type, 99 | object_id: resource_id, 100 | depth: depth + 1 101 | ) 102 | return true 103 | end 104 | elsif evaluate( 105 | subject_type: subject_type, 106 | subject_id: subject_id, 107 | permission: rule.permission, 108 | resource_type: resource_type, 109 | resource_id: resource_id 110 | ) 111 | # Direct permission check on the same resource 112 | return true 113 | end 114 | when Granity::Rules::Any 115 | # Check if any of the subrules match 116 | if rule.rules.any? do |subrule| 117 | evaluate_rules( 118 | rules: [subrule], 119 | subject_type: subject_type, 120 | subject_id: subject_id, 121 | resource_type: resource_type, 122 | resource_id: resource_id, 123 | depth: depth + 1 124 | ) 125 | end 126 | return true 127 | end 128 | when Granity::Rules::All 129 | # Check if all of the subrules match 130 | if rule.rules.all? do |subrule| 131 | evaluate_rules( 132 | rules: [subrule], 133 | subject_type: subject_type, 134 | subject_id: subject_id, 135 | resource_type: resource_type, 136 | resource_id: resource_id, 137 | depth: depth + 1 138 | ) 139 | end 140 | return true 141 | end 142 | end 143 | end 144 | 145 | false 146 | end 147 | 148 | # Check if a direct relation tuple exists 149 | def check_relation(subject_type:, subject_id:, relation:, object_type:, object_id:) 150 | Granity::RelationTuple.exists?( 151 | subject_type: subject_type, 152 | subject_id: subject_id, 153 | relation: relation, 154 | object_type: object_type, 155 | object_id: object_id 156 | ) 157 | end 158 | 159 | # Check relation with traversal (the "from" case) 160 | def check_relation_traversal(subject_type:, subject_id:, relation:, from_relation:, object_type:, object_id:) 161 | # First, find all intermediary objects through the 'from' relation 162 | intermediaries = Granity::RelationTuple.where( 163 | object_type: object_type, 164 | object_id: object_id, 165 | relation: from_relation 166 | ) 167 | 168 | # For each intermediary, check if the subject has the relation to it 169 | intermediaries.any? do |tuple| 170 | Granity::RelationTuple.exists?( 171 | subject_type: subject_type, 172 | subject_id: subject_id, 173 | relation: relation, 174 | object_type: tuple.subject_type, 175 | object_id: tuple.subject_id 176 | ) 177 | end 178 | end 179 | 180 | # Check permission with traversal (the "from" case) 181 | def check_permission_traversal(subject_type:, subject_id:, permission:, from_relation:, object_type:, object_id:, depth:) 182 | # First, find all intermediary objects through the 'from' relation 183 | intermediaries = Granity::RelationTuple.where( 184 | object_type: object_type, 185 | object_id: object_id, 186 | relation: from_relation 187 | ) 188 | 189 | # For each intermediary, check if the subject has the permission on it 190 | intermediaries.any? do |tuple| 191 | evaluate( 192 | subject_type: subject_type, 193 | subject_id: subject_id, 194 | permission: permission, 195 | resource_type: tuple.subject_type, 196 | resource_id: tuple.subject_id 197 | ) 198 | end 199 | end 200 | 201 | # Collect subjects that have a permission on a resource 202 | def collect_subjects_for_permission(rules:, resource_type:, resource_id:) 203 | subjects = [] 204 | 205 | rules.each do |rule| 206 | case rule 207 | when Granity::Rules::Relation 208 | if rule.from 209 | # Handle relation traversal for finding subjects 210 | intermediaries = Granity::RelationTuple.where( 211 | object_type: resource_type, 212 | object_id: resource_id, 213 | relation: rule.from 214 | ) 215 | 216 | intermediaries.each do |tuple| 217 | # For each intermediary, find subjects with the relation 218 | relations = Granity::RelationTuple.where( 219 | object_type: tuple.subject_type, 220 | object_id: tuple.subject_id, 221 | relation: rule.relation 222 | ) 223 | 224 | relations.each do |rel| 225 | subjects << {type: rel.subject_type, id: rel.subject_id} 226 | end 227 | end 228 | else 229 | # Direct relation - find all subjects with this relation 230 | tuples = Granity::RelationTuple.where( 231 | object_type: resource_type, 232 | object_id: resource_id, 233 | relation: rule.relation 234 | ) 235 | 236 | tuples.each do |tuple| 237 | subjects << {type: tuple.subject_type, id: tuple.subject_id} 238 | end 239 | end 240 | when Granity::Rules::Permission 241 | # Find subjects with referenced permission 242 | if rule.from 243 | # Permission with traversal - we would need to traverse the "from" relation 244 | # and then find subjects with the permission on those objects 245 | # This is a simplification - in a real implementation we would handle this more efficiently 246 | intermediaries = Granity::RelationTuple.where( 247 | object_type: resource_type, 248 | object_id: resource_id, 249 | relation: rule.from 250 | ) 251 | 252 | intermediaries.each do |tuple| 253 | perm_subjects = find_subjects( 254 | resource_type: tuple.subject_type, 255 | resource_id: tuple.subject_id, 256 | permission: rule.permission 257 | ) 258 | 259 | subjects.concat(perm_subjects) 260 | end 261 | else 262 | # Direct permission check 263 | referenced_subjects = find_subjects( 264 | resource_type: resource_type, 265 | resource_id: resource_id, 266 | permission: rule.permission 267 | ) 268 | 269 | subjects.concat(referenced_subjects) 270 | end 271 | when Granity::Rules::Any, Granity::Rules::All 272 | # Recursively collect subjects for each subrule 273 | rule.rules.each do |subrule| 274 | subrule_subjects = collect_subjects_for_permission( 275 | rules: [subrule], 276 | resource_type: resource_type, 277 | resource_id: resource_id 278 | ) 279 | 280 | subjects.concat(subrule_subjects) 281 | end 282 | end 283 | end 284 | 285 | # Remove duplicates 286 | subjects.uniq { |s| "#{s[:type]}:#{s[:id]}" } 287 | end 288 | end 289 | end 290 | end 291 | -------------------------------------------------------------------------------- /lib/granity/relation.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | # Definition of a relation between resources 3 | class Relation 4 | attr_reader :name, :resource_type, :target_type, :description 5 | 6 | def initialize(name:, resource_type:, target_type:, description: nil) 7 | @name = name 8 | @resource_type = resource_type 9 | @target_type = target_type 10 | @description = description 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/granity/resource_type.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | # Definition of a resource type in the authorization schema 3 | class ResourceType 4 | attr_reader :name, :relations, :permissions 5 | 6 | def initialize(name) 7 | @name = name.to_sym 8 | @relations = {} 9 | @permissions = {} 10 | end 11 | 12 | # DSL method to define a relation 13 | def relation(name, type:, description: nil) 14 | @relations[name.to_sym] = Relation.new( 15 | name: name.to_sym, 16 | resource_type: @name, 17 | target_type: type.to_sym, 18 | description: description 19 | ) 20 | end 21 | 22 | # DSL method to define a permission 23 | def permission(name, description: nil, &block) 24 | permission = Permission.new( 25 | name: name.to_sym, 26 | resource_type: @name, 27 | description: description 28 | ) 29 | 30 | permission.instance_eval(&block) if block_given? 31 | @permissions[name.to_sym] = permission 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/granity/rules.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | # Rules namespace for permission evaluation rules 3 | module Rules 4 | # Base class for all rules 5 | class Base 6 | def initialize 7 | # Base initialization 8 | end 9 | end 10 | 11 | # Rule for checking relation existence 12 | class Relation < Base 13 | attr_reader :relation, :from 14 | 15 | def initialize(relation:, from: nil) 16 | @relation = relation.to_sym 17 | @from = from.to_sym if from 18 | end 19 | end 20 | 21 | # Rule for checking another permission 22 | class Permission < Base 23 | attr_reader :permission, :from 24 | 25 | def initialize(permission:, from: nil) 26 | @permission = permission.to_sym 27 | @from = from.to_sym if from 28 | end 29 | end 30 | 31 | # Rule container where ANY rule must match 32 | class Any < Base 33 | attr_reader :rules 34 | 35 | def initialize 36 | @rules = [] 37 | end 38 | 39 | def include_relation(relation, from: nil) 40 | @rules << Relation.new(relation: relation, from: from) 41 | end 42 | 43 | def include_permission(permission, from: nil) 44 | @rules << Permission.new(permission: permission, from: from) 45 | end 46 | 47 | def include_any(&block) 48 | rule = Any.new 49 | rule.instance_eval(&block) 50 | @rules << rule 51 | end 52 | 53 | def include_all(&block) 54 | rule = All.new 55 | rule.instance_eval(&block) 56 | @rules << rule 57 | end 58 | end 59 | 60 | # Rule container where ALL rules must match 61 | class All < Base 62 | attr_reader :rules 63 | 64 | def initialize 65 | @rules = [] 66 | end 67 | 68 | def include_relation(relation, from: nil) 69 | @rules << Relation.new(relation: relation, from: from) 70 | end 71 | 72 | def include_permission(permission, from: nil) 73 | @rules << Permission.new(permission: permission, from: from) 74 | end 75 | 76 | def include_any(&block) 77 | rule = Any.new 78 | rule.instance_eval(&block) 79 | @rules << rule 80 | end 81 | 82 | def include_all(&block) 83 | rule = All.new 84 | rule.instance_eval(&block) 85 | @rules << rule 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/granity/schema.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | # Schema definition for Granity authorization model 3 | class Schema 4 | attr_reader :resource_types 5 | 6 | class << self 7 | # DSL entry point for defining schema 8 | def define(&block) 9 | @current = new 10 | @current.instance_eval(&block) 11 | @current 12 | end 13 | 14 | # Get current schema 15 | def current 16 | @current ||= new 17 | end 18 | end 19 | 20 | def initialize 21 | @resource_types = {} 22 | end 23 | 24 | # DSL method to define a resource type 25 | def resource_type(name, &block) 26 | resource_type = ResourceType.new(name) 27 | resource_type.instance_eval(&block) if block_given? 28 | @resource_types[name] = resource_type 29 | end 30 | 31 | def validate_schema 32 | # Validate that all relation types reference valid resource types 33 | @resource_types.each do |name, resource_type| 34 | resource_type.relations.each do |relation_name, relation| 35 | unless @resource_types.key?(relation.type.to_sym) 36 | raise SchemaError, "Resource type '#{name}' has relation '#{relation_name}' with invalid type '#{relation.type}'" 37 | end 38 | end 39 | end 40 | end 41 | end 42 | 43 | class SchemaError < StandardError; end 44 | end 45 | -------------------------------------------------------------------------------- /lib/granity/version.rb: -------------------------------------------------------------------------------- 1 | module Granity 2 | VERSION = "0.1.3" 3 | end 4 | -------------------------------------------------------------------------------- /lib/tasks/granity_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :granity do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. 3 | allow_browser versions: :modern 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |The server cannot process the request due to a client error. Please check the request and try again. If you’re the application owner check the logs for more information.
109 |The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.
109 |Your browser is not supported.
Please upgrade your browser to continue.
The change you wanted was rejected. Maybe you tried to change something you didn’t have access to. If you’re the application owner check the logs for more information.
109 |We’re sorry, but something went wrong.
If you’re the application owner check the logs for more information.