/saas_rbac

RBAC APIs for SAAS based services in Rust

Primary LanguageRustMIT LicenseMIT

SaasRRBAC - RBAC implementation for SAAS based applications, written in Rust

Overview

SaasRRBAC provides APIs to add support for role and claim based security. It supports both static and instance based security.

Features:

  • Multi-tenancy support for multiple organizations and users within those organizations
  • Organizations can have multiple users (principals) and groups
  • Role based security
  • Roles can be inherited
  • Roles can be associated with principals or groups of organizations
  • Claim based security, where claims define permittable actions and can be associated directly with principals or via roles that are associated to principals.
  • Instance/Constraints based security using dynamic context.
  • Roles and claims can be assigned within a range of time period.
  • Restrict access based on geo-fencing
  • Resource accountings, where you can enforce quota-limit and track usage by users and use them as part of instance based security.

Requirements:

  • Rust

Version

  • 0.1

License

  • MIT

Design:

Realm

The realm defines domain of security, e.g. you may have have different security policies for different applications that can be applied by creating realm for each application.

Organization

An organization represents customer who can have one or more principals (users) and groups.

Group

A group represents segregation of responsibility within the organization and can be associated with one or more principals (users).

Principal

A principal represents an identity and tied with the organization.

Role

A role represents job title or function. A principal belongs to one or more roles. One of key feature of SaasRRBAC is that roles support inheritance where a role can have one or more roles. Roles can be assigned for a predefined duration of time to principals.

Resource

A resource represents the entity that needs to be protected such as APIs, Data, Feature-set, reports, jobs, projects, etc.

Claim

A claim defines permission and consists of three parts: operation, resource-id and constraints, where operation is a "verb" that describes action and resource-id represents id of the resource that is acted upon, and constraints is an optional component that describes dynamic condition that must be checked. The claims can be assigned to roles or principal. The constraints contains a logical expressions and provides access to runtime request parameters. Claim can be assigned for a duration of time so that they are not permanent.

License Policy

The license policy represents a set of claims that an organization can access based on pricing or license model.

Note: The resources and claims are defined by the Saas provider and then sign up process defines organization and license-policy. The organization then creates principals/roles and associates claims with roles/principals. All claims set by the organization would be subset of license policy and time bound within the range of license policy.

System Layers

SaasRRBAC consists of following layers

Business Domain Layer

This layer defines core classes that are part of the RBAC based security realm such as:

  • Realm – - The realm allows you to support multiple applications or security realms.
  • Organization – The organization represents the organization that customer belongs to, each customer may belong to different organizations.
  • Principal – The principal represents an identity of users that belong to customer organizations. Note, this object represents employees/users of customers and not users of hosting provider, though they can be modeled in similar fashion.
  • LicensePolicy – The license policy represents overall access for the customers and it's mapped to claims.
  • Group – A group represents departments/groups within the organizaiton.
  • Role – A role represents job title or function.
  • Resource - The resource represents object that needs to be protected such as APIs, files, data, reports, etc.
  • Claim – A claim is tied with resources and defines operation and constraints that need to be inforced. It may define dynamic properties for for dynamic or instance based security.
  • SecurityManager – Checks access permission for principals.

Repository Layer

This layer is responsible for accessing or storing above objects in the database. SaasRRBAC uses Sqlite by default but it can be easily mapped to other databases. Following are list of repositories supported by SaasRRBAC:

  • PersistenceManager – provides high level methods to persist or query domain objects for security
  • RealmRepository – provides database access for Realms.
  • ClaimRepository – provides database access for Claims.
  • PrincipalRepository – provides database access for Principals.
  • RoleRepository – provides database access for Roles.
  • GroupRepository – provides database access for Groups.
  • LicensePolicyRepository – provides database access for license-policy

Security Layer

This layer defines SecurityManager for validating authorization policies.

Evaluation Layer

This layer proivdes evaluation engine for supporting instance based security.

REST API Service Layer

This layer defines REST services such as:

  • RealmService – this service provides REST APIs for accessing Realms.
  • OrganizationService – this service provides REST APIs for accessing Organizations
  • PrincipalService – this service provides REST APIs for accessing Principals.
  • LicensePolicyService – this service provides REST APIs for accessing license policies.
  • RoleService – this service provides REST APIs for accessing Roles.
  • ResourceService – this service provides REST APIs for accessing resources, instances, and quota-limits.
  • ClaimService – this service provides REST APIs for accessing Claims.
  • SecurityService – this service provides REST APIs for authorizing claims.

Caching Layer

This layer provides caching security claims to improve performance.

Setup

  • Install rust
rustup override set nightly
rustup update && cargo update
  • Run migrations
cargo install diesel_cli --no-default-features --features sqlite
cargo install cargo-tree
echo DATABASE_URL=db.qlite > .env
diesel setup --database-url db.sqlite
diesel migration run
diesel print-schema > src/plexrbac/persistence/schema.rs

Note: By default, SaasRRBAC works with sqlite but you can update Cargo.toml and above command to use postgres or mysql. Note: You can re-apply migrations using diesel migration redo (See https://sqliteonline.com/)

  • Run Tests
cargo test -- --test-threads=1
  • Run Test Coverage
cargo install cargo-kcov
brew install cmake jq
cargo kcov --print-install-kcov-sh | sh
cd kcov-v36
cmake -G Xcode
xcodebuild -configuration Release
cd back-to-rbac-folder
cargo kcov
  • Docs
rustc doc.rs --crate-type lib
rustdoc --test --extern doc="libdoc.rlib" doc.rs

Use Cases

Banking

Let’s start with a banking example where a bank-object can be account, general-ledger-report or ledger-posting-rules and account is further grouped into customer account or loan account. Further, Let’s assume there are five roles: Teller, Customer-Service-Representative (CSR), Account, AccountingManager and LoanOfficer, where

  • A teller can modify customer deposit accounts — but only if customer and teller live in same region
  • A customer service representative can create or delete customer deposit accounts — but only if customer and teller live in same region
  • An accountant can create general ledger reports — but only if year is h2. current year
  • An accounting manager can modify ledger-posting rules — but only if year is h2. current year
  • A loan officer can create and modify loan accounts – but only if account balance is < 10000

Initialize context and repository

let ctx = SecurityContext::new("0".into(), "0".into());
let cf = DefaultConnectionFactory::new();
let locator = RepositoryFactory::new(&cf);
let pm = locator.new_persistence_manager();

Creating security realm

let realm = pm.new_realm_with(&ctx, "banking")?;

Creating organization

let org = pm.new_org_with(&ctx, "bank-of-flakes")?;

Creating Users

let tom = pm.new_principal_with(&ctx, &org, "tom")?;
let cassy = pm.new_principal_with(&ctx, &org, "cassy")?;
let ali = pm.new_principal_with(&ctx, &org, "ali")?;
let mike = pm.new_principal_with(&ctx, &org, "mike")?;
let larry = pm.new_principal_with(&ctx, &org, "larry")?;

Creating Roles

let employee = pm.new_role_with(&ctx, &realm, &org, "Employee")?;
let teller = pm.new_role_with_parent(&ctx, &realm, &org, &employee, "Teller")?;
let csr = pm.new_role_with_parent(&ctx, &realm, &org, &teller, "CSR")?;
let accountant = pm.new_role_with_parent(&ctx, &realm, &org, &employee, "Accountant")?;
let accountant_manager = pm.new_role_with_parent(&ctx, &realm, &org, &accountant, "AccountingManager")?;
let loan_officer = pm.new_role_with_parent(&ctx, &realm, &org, &accountant_manager, "LoanOfficer")?;

Creating Resources

let deposit_account = pm.new_resource_with(&ctx, &realm, "DepositAccount")?;
let loan_account = pm.new_resource_with(&ctx, &realm, "LoanAccount")?;
let general_ledger = pm.new_resource_with(&ctx, &realm, "GeneralLedger")?;
let posting_rules = pm.new_resource_with(&ctx, &realm, "GeneralLedgerPostingRules")?;

Creating claims for resources

let cd_deposit = pm.new_claim_with(&ctx, &realm, &deposit_account, "(CREATE|DELETE)")?;
let ru_deposit = pm.new_claim_with(&ctx, &realm, &deposit_account, "(READ|UPDATE)")?;

let cd_loan = pm.new_claim_with(&ctx, &realm, &loan_account, "(CREATE|DELETE)")?;
let ru_loan = pm.new_claim_with(&ctx, &realm, &loan_account, "(READ|UPDATE)")?;

let rd_ledger = pm.new_claim_with(&ctx, &realm, &general_ledger, "(READ|CREATE|DELETE)")?;
let r_glpr = pm.new_claim_with(&ctx, &realm, &general_ledger, "(READ)")?;

let cud_glpr = pm.new_claim_with(&ctx, &realm, &posting_rules, "(CREATE|UPDATE|DELETE)")?;

Mapping Principals and Claims to Roles

pm.map_principal_to_role(&ctx, &tom, &teller);
pm.map_principal_to_role(&ctx, &cassy, &csr);
pm.map_principal_to_role(&ctx, &ali, &accountant);
pm.map_principal_to_role(&ctx, &mike, &accountant_manager);
pm.map_principal_to_role(&ctx, &larry, &loan_officer);

Map claims to roles as follows:

pm.map_role_to_claim(&ctx, &teller, &ru_deposit, "U.S.", r#"employeeRegion == "Midwest""#);
pm.map_role_to_claim(&ctx, &csr, &cd_deposit, "U.S.", r#"employeeRegion == "Midwest""#);
pm.map_role_to_claim(&ctx, &accountant, &rd_ledger, "U.S.", r#"employeeRegion == "Midwest" && ledgerYear == current_year()"#);
pm.map_role_to_claim(&ctx, &accountant, &ru_loan, "U.S.", r#"employeeRegion == "Midwest" && accountBlance < 10000"#);
pm.map_role_to_claim(&ctx, &accountant_manager, &cd_loan, "U.S.", r#"employeeRegion == "Midwest" && accountBlance < 10000"#);
pm.map_role_to_claim(&ctx, &accountant_manager, &r_glpr, "U.S.", r#"employeeRegion == "Midwest" && ledgerYear == current_year()"#);
pm.map_role_to_claim(&ctx, &loan_officer, &cud_glpr, "U.S.", r#"employeeRegion == "Midwest" && ledgerYear == current_year()"#);

Checking permissions

let sm = SecurityManager::new(pm);

Tom, the teller should be able to READ DepositAccount with scope U.S when employeeRegion == Midwest

let mut req = PermissionRequest::new(realm.id.as_str(), tom.id.as_str(), ActionType::READ, "DepositAccount", "U.S.");
req.context.add("employeeRegion", ValueWrapper::String("Midwest".to_string()));
assert_eq!(PermissionResult::Allow, sm.check(&req)?);

Tom, the teller should not be able to READ DepositAccount with scope U.S when employeeRegion == Northeast

let mut req = PermissionRequest::new(realm.id.as_str(), tom.id.as_str(), ActionType::READ, "DepositAccount", "U.S.");
req.context.add("employeeRegion", ValueWrapper::String("Northeast".to_string()));
assert!(sm.check(&req).is_err());

Tom, the teller should not be able to DELETE DepositAccount with scope U.S when employeeRegion == Midwest

let mut req = PermissionRequest::new(realm.id.as_str(), tom.id.as_str(), ActionType::DELETE, "DepositAccount", "U.S.");
req.context.add("employeeRegion", ValueWrapper::String("Midwest".to_string()));
assert!(sm.check(&req).is_err());

Cassy, the CSR should be able to DELETE DepositAccount with scope U.S when employeeRegion == Midwest

let mut req = PermissionRequest::new(realm.id.as_str(), cassy.id.as_str(), ActionType::DELETE, "DepositAccount", "U.S.");
req.context.add("employeeRegion", ValueWrapper::String("Midwest".to_string()));
assert_eq!(PermissionResult::Allow, sm.check(&req)?);

Cassy, the CSR should be able to DELETE DepositAccount with scope U.K when employeeRegion == Midwest

let mut req = PermissionRequest::new(realm.id.as_str(), cassy.id.as_str(), ActionType::DELETE, "DepositAccount", "U.K.");
req.context.add("employeeRegion", ValueWrapper::String("Midwest".to_string()));
assert!(sm.check(&req).is_err());

Ali, the Accountant should be able to READ GeneralLedger with scope U.S when employeeRegion == Midwest AND ledgerYear == current_year()

let mut req = PermissionRequest::new(realm.id.as_str(), ali.id.as_str(), ActionType::READ, "GeneralLedger", "U.S.");
req.context.add("employeeRegion", ValueWrapper::String("Midwest".to_string()));
req.context.add("ledgerYear", ValueWrapper::Int(Utc::now().naive_utc().year() as i64));
assert_eq!(PermissionResult::Allow, sm.check(&req)?);

Ali, the Accountant should not be able to READ GeneralLedger with scope U.S when employeeRegion == Midwest AND ledgerYear is in past

req.context.add("ledgerYear", ValueWrapper::Int(2000));
assert!(sm.check(&req).is_err());

Ali, the Accountant should not be able to DELETE GeneralLedger with scope U.S when employeeRegion == Midwest AND ledgerYear == current_year()

let mut req = PermissionRequest::new(realm.id.as_str(), ali.id.as_str(), ActionType::DELETE, "GeneralLedger", "U.S.");
req.context.add("employeeRegion", ValueWrapper::String("Midwest".to_string()));
req.context.add("ledgerYear", ValueWrapper::Int(Utc::now().naive_utc().year() as i64));
assert!(sm.check(&req).is_err());

Mike, the Accountant Manager should be able to DELETE GeneralLedger with scope U.S when employeeRegion == Midwest AND ledgerYear == current_year()

let mut req = PermissionRequest::new(realm.id.as_str(), mike.id.as_str(), ActionType::CREATE, "GeneralLedger", "U.S.");
req.context.add("employeeRegion", ValueWrapper::String("Midwest".to_string()));
req.context.add("ledgerYear", ValueWrapper::Int(Utc::now().naive_utc().year() as i64));
assert_eq!(PermissionResult::Allow, sm.check(&req)?);

Mike, the Accountant Manager should not be able to post posting-rules of general-ledger with scope U.S when employeeRegion == Midwest AND ledgerYear == current_year()

let mut req = PermissionRequest::new(realm.id.as_str(), mike.id.as_str(), ActionType::CREATE, "GeneralLedgerPostingRules", "U.S.");
req.context.add("employeeRegion", ValueWrapper::String("Midwest".to_string()));
req.context.add("ledgerYear", ValueWrapper::Int(Utc::now().naive_utc().year() as i64));
req.context.add("accountBlance", ValueWrapper::Int(500));
assert!(sm.check(&req).is_err());

Larry, the Loan Officer should be able to post posting-rules of general-ledger with scope U.S when employeeRegion == Midwest AND ledgerYear == current_year()

let mut req = PermissionRequest::new(realm.id.as_str(), larry.id.as_str(), ActionType::CREATE, "GeneralLedgerPostingRules", "U.S.");
req.context.add("employeeRegion", ValueWrapper::String("Midwest".to_string()));
req.context.add("ledgerYear", ValueWrapper::Int(Utc::now().naive_utc().year() as i64));
req.context.add("accountBlance", ValueWrapper::Int(500));
assert_eq!(PermissionResult::Allow, sm.check(&req)?);

Expense Report

Creating security realm

let realm = pm.new_realm_with(&ctx, "expense")?;

Creating organization

let org = pm.new_org_with(&ctx, "box-air")?;

Creating Groups

let group_employee = pm.new_group_with(&ctx, &org, "Employee")?;
let group_manager = pm.new_group_with_parent(&ctx, &org, &group_employee, "Manager")?;

Creating Users

let tom = pm.new_principal_with(&ctx, &org, "tom")?;
let mike = pm.new_principal_with(&ctx, &org, "mike")?;

Mapping users to groups

pm.map_principal_to_group(&ctx, &tom, &group_employee);
pm.map_principal_to_group(&ctx, &mike, &group_employee);
pm.map_principal_to_group(&ctx, &mike, &group_manager);

Creating Roles

let employee = pm.new_role_with(&ctx, &realm, &org, "Employee")?;
let manager = pm.new_role_with_parent(&ctx, &realm, &org, &employee, "Manager")?;

Creating Resources

let expense_report = pm.new_resource_with(&ctx, &realm, "ExpenseReport")?;

Creating claims for resources

let submit_report = pm.new_claim_with(&ctx, &realm, &expense_report, "(SUBMIT|VIEW)")?;
let approve_report = pm.new_claim_with(&ctx, &realm, &expense_report, "APPROVE")?;

Mapping Principals and Claims to Roles

pm.map_group_to_role(&ctx, &group_employee, &employee, "");
pm.map_group_to_role(&ctx, &group_manager, &manager, "");

Map claims to roles as follows:

pm.map_role_to_claim(&ctx, &employee, &submit_report, "U.S.", r#"amount < 10000"#);
pm.map_role_to_claim(&ctx, &manager, &approve_report, "U.S.", r#"amount < 10000"#);

Checking Permissions

let sm = SecurityManager::new(pm);

Tom should be able to submit report

let mut req = PermissionRequest::new(realm.id.as_str(), tom.id.as_str(), ActionType::SUBMIT, "ExpenseReport", "U.S.");
req.context.add("amount", ValueWrapper::Int(1000));
assert_eq!(PermissionResult::Allow, sm.check(&req)?);

Tom should not be able to approve report

let mut req = PermissionRequest::new(realm.id.as_str(), tom.id.as_str(), ActionType::APPROVE, "ExpenseReport", "U.S.");
req.context.add("amount", ValueWrapper::Int(1000));
assert!(sm.check(&req).is_err());

Mike should be able to approve report

let mut req = PermissionRequest::new(realm.id.as_str(), mike.id.as_str(), ActionType::APPROVE, "ExpenseReport", "U.S.");
req.context.add("amount", ValueWrapper::Int(1000));
assert_eq!(PermissionResult::Allow, sm.check(&req)?);

Feature flag with Geo-Fencing

Initialize context and repository

let ctx = SecurityContext::new("0".into(), "0".into());
let cf = DefaultConnectionFactory::new();
let locator = RepositoryFactory::new(&cf);
let pm = locator.new_persistence_manager();

Creating security realm

let realm = pm.new_realm_with(&ctx, "ada")?;

Creating organization

let org = pm.new_org_with(&ctx, "ada")?;

Creating Users

let tom = pm.new_principal_with(&ctx, &org, "tom")?;
let mike = pm.new_principal_with(&ctx, &org, "mike")?;

Creating Roles

let customer = pm.new_role_with(&ctx, &realm, &org, "Customer")?;
let beta_customer = pm.new_role_with_parent(&ctx, &realm, &org, &customer, "BetaCustomer")?;

Creating Resources

let feature = pm.new_resource_with(&ctx, &realm, "Feature")?;

Creating claims for resources

let view = pm.new_claim_with(&ctx, &realm, &feature, "VIEW")?;

Mapping Principals and Claims to Roles

pm.map_principal_to_role(&ctx, &tom, &customer);
pm.map_principal_to_role(&ctx, &mike, &beta_customer);

Map claims to roles as follows:

pm.map_role_to_claim(&ctx, &customer, &view, "UI::Flag::BasicReport", r#"geo_distance_km(customer_lat, customer_lon, 47.620422, -122.349358) < 100"#);
pm.map_role_to_claim(&ctx, &beta_customer, &view, "UI::Flag::AdvancedReport", r#"geo_distance_km(customer_lat, customer_lon, 47.620422, -122.349358) < 200"#);

Tom should be able to view basic report if he lives close to Seattle

let mut req = PermissionRequest::new(realm.id.as_str(), tom.id.as_str(), ActionType::VIEW, "Feature", "UI::Flag::BasicReport");
req.context.add("customer_lat", ValueWrapper::Float(46.879967));
req.context.add("customer_lon", ValueWrapper::Float(-121.726906));
assert_eq!(PermissionResponse::Allow, sm.check(&req)?);

Tom should not be able to view basic report if he lives far from Seattle

let mut req = PermissionRequest::new(realm.id.as_str(), tom.id.as_str(), ActionType::VIEW, "Feature", "UI::Flag::BasicReport");
req.context.add("customer_lat", ValueWrapper::Float(37.3230));
req.context.add("customer_lon", ValueWrapper::Float(-122.0322));
assert!(sm.check(&req).is_err());

Tom should not be able to view advanced report

let mut req = PermissionRequest::new(realm.id.as_str(), tom.id.as_str(), ActionType::VIEW, "Feature", "UI::Flag::AdvancedReport");
req.context.add("customer_lat", ValueWrapper::Float(46.879967));
req.context.add("customer_lon", ValueWrapper::Float(-121.726906));
assert!(sm.check(&req).is_err());

Mike should be able to view advanced report

let mut req = PermissionRequest::new(realm.id.as_str(), mike.id.as_str(), ActionType::VIEW, "Feature", "UI::Flag::AdvancedReport");
req.context.add("customer_lat", ValueWrapper::Float(46.879967));
req.context.add("customer_lon", ValueWrapper::Float(-121.726906));
assert_eq!(PermissionResponse::Allow, sm.check(&req)?);

Mike should not be able to view advanced report if he lives far from Seattle

let mut req = PermissionRequest::new(realm.id.as_str(), mike.id.as_str(), ActionType::VIEW, "Feature", "UI::Flag::AdvancedReport");
req.context.add("customer_lat", ValueWrapper::Float(37.3230));
req.context.add("customer_lon", ValueWrapper::Float(-122.0322));
assert!(sm.check(&req).is_err());

Using License Policy to restrict access to different level of features

// Creating organization

let freemium_org = pm.new_org_with(&ctx, "Freeloader")?;
let paid_org = pm.new_org_with(&ctx, "Moneymaker")?;

Create license policies

let freemium_policy = pm.new_license_policy(&ctx, &freemium_org)?;
let paid_policy = pm.new_license_policy(&ctx, &paid_org)?;

Creating Users

let freemium_frank = pm.new_principal_with(&ctx, &freemium_org, "frank")?;
let money_matt = pm.new_principal_with(&ctx, &paid_org, "matt")?;

Creating Roles

let customer = pm.new_role_with(&ctx, &realm, &freemium_org, "Customer")?;
let paid_customer = pm.new_role_with(&ctx, &realm, &paid_org, "PaidCustomer")?;

Creating Resources

let feature = pm.new_resource_with(&ctx, &realm, "Feature")?;

Creating claims for resources

let view = pm.new_claim_with(&ctx, &realm, &feature, "VIEW")?;

Mapping Principals and Claims to Roles

pm.map_principal_to_role(&ctx, &freemium_frank, &customer);
pm.map_principal_to_role(&ctx, &money_matt, &customer);
pm.map_principal_to_role(&ctx, &money_matt, &paid_customer);

Map claims to policies as follows:

pm.map_license_policy_to_claim(&ctx, &freemium_policy, &view, "UI::Flag::BasicReport", "");
pm.map_license_policy_to_claim(&ctx, &paid_policy, &view, "UI::Flag::AdvancedReport", "");

Map claims to roles as follows:

pm.map_role_to_claim(&ctx, &customer, &view, "UI::Flag::BasicReport", "");
pm.map_role_to_claim(&ctx, &paid_customer, &view, "UI::Flag::AdvancedReport", "");

Frank should be able to view basic report

let req = PermissionRequest::new(realm.id.as_str(), freemium_frank.id.as_str(), ActionType::VIEW, "Feature", "UI::Flag::BasicReport");
assert_eq!(PermissionResponse::Allow, sm.check(&req)?);

Frank should not be able to view advanced report

let req = PermissionRequest::new(realm.id.as_str(), freemium_frank.id.as_str(), ActionType::VIEW, "Feature", "UI::Flag::AdvancedReport");
assert!(sm.check(&req).is_err());

Matt should be able to view advanced report

let req = PermissionRequest::new(realm.id.as_str(), money_matt.id.as_str(), ActionType::VIEW, "Feature", "UI::Flag::AdvancedReport");
assert_eq!(PermissionResponse::Allow, sm.check(&req)?);

Using Data Access

Creating security realm

let realm = mgr.new_realm_with(&ctx, "dada").unwrap();

Creating organization

let org = mgr.new_org_with(&ctx, "dada").unwrap();

Creating Users

let tom = mgr.new_principal_with(&ctx, &org, "tom").unwrap();
let mike = mgr.new_principal_with(&ctx, &org, "mike").unwrap();

Creating Roles

let customer = mgr.new_role_with(&ctx, &realm, &org, "Customer").unwrap();
let beta_customer = mgr.new_role_with_parent(&ctx, &realm, &org, &customer, "BetaCustomer").unwrap();

Creating Resources

let data = mgr.new_resource_with(&ctx, &realm, "Data").unwrap();

Creating claims for resources

let view = mgr.new_claim_with(&ctx, &realm, &data, "VIEW").unwrap();

Mapping Principals and Claims to Roles

mgr.map_principal_to_role(&ctx, &tom, &customer);
mgr.map_principal_to_role(&ctx, &mike, &beta_customer);

Map claims to roles as follows:

mgr.map_role_to_claim(&ctx, &customer, &view, "Report::Summary", "");
mgr.map_role_to_claim(&ctx, &beta_customer, &view, "Report::Details", "");

Tom should be able to view summary

let security_mgr = SecurityManager::new(mgr);
let mut req = PermissionRequest::new(realm.id.as_str(), tom.id.as_str(), ActionType::VIEW, "Data", "Report::Summary");
if PermissionResponse::Allow == security_mgr.check(&req)? {
    // add summary data
}

Tom should not be able to view details

let mut req = PermissionRequest::new(realm.id.as_str(), tom.id.as_str(), ActionType::VIEW, "Data", "Report::Details");
assert!(security_mgr.check(&req).is_err());

Mike should be able to view details

let mut req = PermissionRequest::new(realm.id.as_str(), mike.id.as_str(), ActionType::VIEW, "Data", "Report::Details");
if PermissionResponse::Allow == security_mgr.check(&req)? {
    // add details data
}

Using Multiple teams for roles

Create license policies

let policy = pm.new_license_policy(&ctx, &org)?;

Creating Users

let dave = pm.new_principal_with(&ctx, &org, "dave")?;
let qari = pm.new_principal_with(&ctx, &org, "qari")?;
let ali = pm.new_principal_with(&ctx, &org, "ali")?;

Creating Roles

let developer = pm.new_role_with(&ctx, &realm, &org, "Developer")?;
let qa = pm.new_role_with(&ctx, &realm, &org, "QA")?;
let admin = pm.new_role_with_parent(&ctx, &realm, &org, &developer, "Admin")?;

Creating Resources

let app = pm.new_resource_with(&ctx, &realm, "App")?;

Creating claims for resources

let submit_view = pm.new_claim_with(&ctx, &realm, &app, "(SUBMIT|VIEW)")?;
let view = pm.new_claim_with(&ctx, &realm, &app, "VIEW")?;
let create_delete = pm.new_claim_with(&ctx, &realm, &app, "(CREATE|DELETE)")?;

Mapping Principals and Claims to Roles

pm.map_principal_to_role(&ctx, &dave, &developer);
pm.map_principal_to_role(&ctx, &qari, &qa);
pm.map_principal_to_role(&ctx, &ali, &admin);

Map claims to policies as follows:

pm.map_license_policy_to_claim(&ctx, &policy, &submit_view, "com.xyz.app", "appSize < 1000");
pm.map_license_policy_to_claim(&ctx, &policy, &view, "com.xyz.app", "appSize < 1000");
pm.map_license_policy_to_claim(&ctx, &policy, &create_delete, "com.xyz.app", "");

Map claims to roles as follows:

pm.map_role_to_claim(&ctx, &developer, &submit_view, "com.xyz.app", "appSize < 1000");
pm.map_role_to_claim(&ctx, &qa, &view, "com.xyz.app", "appSize < 1000");
pm.map_role_to_claim(&ctx, &admin, &create_delete, "com.xyz.app", "");

Dave should be able to submit app

let mut req = PermissionRequest::new(realm.id.as_str(), dave.id.as_str(), ActionType::SUBMIT, "App", "com.xyz.app");
req.context.add("appSize", ValueWrapper::Int(500));
assert_eq!(PermissionResponse::Allow, sm.check(&req)?);

Qari should be able to view app

let mut req = PermissionRequest::new(realm.id.as_str(), qari.id.as_str(), ActionType::VIEW, "App", "com.xyz.app");
req.context.add("appSize", ValueWrapper::Int(500));
assert_eq!(PermissionResponse::Allow, sm.check(&req)?);

Qari should not be able to create app

let mut req = PermissionRequest::new(realm.id.as_str(), qari.id.as_str(), ActionType::CREATE, "App", "com.xyz.app");
req.context.add("appSize", ValueWrapper::Int(500));
assert!(sm.check(&req).is_err());

Ali should be able to create app

let mut req = PermissionRequest::new(realm.id.as_str(), ali.id.as_str(), ActionType::CREATE, "App", "com.xyz.app");
req.context.add("appSize", ValueWrapper::Int(500));
assert_eq!(PermissionResponse::Allow, sm.check(&req)?);

Ali should be able to submit app

let mut req = PermissionRequest::new(realm.id.as_str(), ali.id.as_str(), ActionType::SUBMIT, "App", "com.xyz.app");
req.context.add("appSize", ValueWrapper::Int(500));
assert_eq!(PermissionResponse::Allow, sm.check(&req)?);

Ali should not be able to submit app with large app

let mut req = PermissionRequest::new(realm.id.as_str(), ali.id.as_str(), ActionType::SUBMIT, "App", "com.xyz.app");
req.context.add("appSize", ValueWrapper::Int(5000));
assert!(sm.check(&req).is_err());

Managing Resource Quota

Creating security realm

let realm = pm.new_realm_with(&ctx, "JobGrid").unwrap();

Creating organization

let abc_corp = pm.new_org_with(&ctx, "ABC").unwrap();
let xyz_corp = pm.new_org_with(&ctx, "XYZ").unwrap();

Create license policies

let abc_policy = pm.new_license_policy(&ctx, &abc_corp).unwrap();
let xyz_policy = pm.new_license_policy(&ctx, &xyz_corp).unwrap();

Creating Resources

let project = pm.new_resource_with(&ctx, &realm, "Project").unwrap();
let job = pm.new_resource_with(&ctx, &realm, "Job").unwrap();

Set Resource Quota for abc corp to setup 1 project and 2 jobs

assert!(pm.new_resource_quota_with(&ctx, &project, &abc_policy, "ABC Project", 1).is_ok());
assert!(pm.new_resource_quota_with(&ctx, &job, &abc_policy, "ABC Jobs", 2).is_ok());

Set Resource Quota for xyz corp to setup 2 project and 3 jobs

assert!(pm.new_resource_quota_with(&ctx, &project, &xyz_policy, "XYZ Project", 2).is_ok());
assert!(pm.new_resource_quota_with(&ctx, &job, &xyz_policy, "XYZ Jobs", 3).is_ok());

abc corp can have at most 1 project

assert!(pm.new_resource_instance_with(&ctx, &project, &abc_policy, "ABC Project", "1", Status::COMPLETED).is_ok());
assert!(pm.new_resource_instance_with(&ctx, &project, &abc_policy, "ABC Project", "2", Status::COMPLETED).is_err());

abc corp can have at most 2 jobs

assert!(pm.new_resource_instance_with(&ctx, &job, &abc_policy, "ABC Jobs", "1", Status::COMPLETED).is_ok());
assert!(pm.new_resource_instance_with(&ctx, &job, &abc_policy, "ABC Jobs", "2", Status::COMPLETED).is_ok());
assert!(pm.new_resource_instance_with(&ctx, &job, &abc_policy, "ABC Jobs", "3", Status::COMPLETED).is_err());

xyz corp can have at most 2 project

assert!(pm.new_resource_instance_with(&ctx, &project, &xyz_policy, "XYZ Project", "1", Status::COMPLETED).is_ok());
assert!(pm.new_resource_instance_with(&ctx, &project, &xyz_policy, "XYZ Project", "2", Status::COMPLETED).is_ok());
assert!(pm.new_resource_instance_with(&ctx, &project, &xyz_policy, "XYZ Project", "3", Status::COMPLETED).is_err());

xyz corp can have at most 3 jobs

assert!(pm.new_resource_instance_with(&ctx, &job, &xyz_policy, "XYZ Jobs", "1", Status::COMPLETED).is_ok());
assert!(pm.new_resource_instance_with(&ctx, &job, &xyz_policy, "XYZ Jobs", "2", Status::COMPLETED).is_ok());
assert!(pm.new_resource_instance_with(&ctx, &job, &xyz_policy, "XYZ Jobs", "3", Status::COMPLETED).is_ok());
assert!(pm.new_resource_instance_with(&ctx, &job, &xyz_policy, "XYZ Jobs", "4", Status::COMPLETED).is_err());

REST APIs

You can start REST API as follows:

export RUST_LOG="warn"
cargo run

Following are major APIs:

Realms

  • Query realms: GET /api/realms
  • Create realm: POST /api/realms
  • Update realm: PUT /api/realms/
  • Find realm: GET /api/realms/
  • Delete realm: DELETE /api/realms/

Resources

  • Query resources: GET /api/realms/<realm_id>/resources
  • Create resource: POST /api/realms/<realm_id>/resources
  • Update resource: PUT /api/realms/<realm_id>/resources/
  • Find resource: GET /api/realms/<realm_id>/resources/
  • Delete resource: DELETE /api/realms/<realm_id>/resources/

Resource Quota

  • Query quota: GET /api/realms/<realm_id>/resources/<resource_id>/quota
  • Create quota: POST /api/realms/<realm_id>/resources/<resource_id>/quota
  • Update quota: PUT /api/realms/<realm_id>/resources/<resource_id>/quota/
  • Find quota: GET /api/realms/<realm_id>/resources/<resource_id>/quota/
  • Delete quota: DELETE /api/realms/<realm_id>/resources/<resource_id>/quota/

Resource Instances

  • Query instances: GET /api/realms/<realm_id>/resources/<resource_id>/instances
  • Create resource instance: POST /api/realms/<realm_id>/resources/<resource_id>/instances
  • Update resource instance: PUT /api/realms/<realm_id>/resources/<resource_id>/instances/
  • Find resource instance: GET /api/realms/<realm_id>/resources/<resource_id>/instances/
  • Delete resource instance: DELETE /api/realms/<realm_id>/resources/<resource_id>/instances/

Claims

  • Query claims within realm: GET /api/realms/<realm_id>/claims
  • Query claims within resource: GET /api/realms/<realm_id>/resources/<resource_id>/claims
  • Create claim: POST /api/realms/<realm_id>/resources/<resource_id>/claims
  • Update claim: PUT /api/realms/<realm_id>/resources/<resource_id>/claims/
  • Find claim: GET /api/realms/<realm_id>/resources/<resource_id>/claims/
  • Delete claim: DELETE /api/realms/<realm_id>/resources/<resource_id>/claims/
  • Add principal to claim: PUT /api/realms/<realm_id>/resources/<resource_id>/claims/<claim_id>/principals/<principal_id>
  • Delete principal from claim: DELETE /api/realms/<realm_id>/resources/<resource_id>/claims/<claim_id>/principals/<principal_id>
  • Add role to claim: PUT /api/realms/<realm_id>/resources/<resource_id>/claims/<claim_id>/roles/<role_id>
  • Delete role from claim: DELETE /api/realms/<realm_id>/resources/<resource_id>/claims/<claim_id>/roles/<role_id>
  • Add claim to license policy: PUT /api/realms/<realm_id>/resources/<resource_id>/claims/<claim_id>/licenses/<license_policy_id>
  • Remove claim from license policy: DELETE /api/realms/<realm_id>/resources/<resource_id>/claims/<claim_id>/licenses/<license_policy_id>

Organizations

  • Query all organizations: GET /api/orgs
  • Create organization: POST /api/orgs
  • Update organization: PUT /api/orgs/
  • Find organization: GET /api/orgs/
  • Delete organization: DELETE /api/orgs/

Groups

  • Query all groups: GET /api/orgs/<org_id>/groups
  • Create group: POST /api/orgs/<org_id>/groups
  • Update group: PUT /api/orgs/<org_id>/groups/
  • Find group: GET /api/orgs/<org_id>/groups/
  • Delete group: DELETE /api/orgs/<org_id>/groups/
  • Add principal to group: PUT /api/orgs/<org_id>/groups/<group_id>/principals/<principal_id>
  • Remove principal from group: DELETE /api/orgs/<org_id>/groups/<group_id>/principals/<principal_id>

Roles

  • Query all roles: GET /api/orgs/<org_id>/roles
  • Create role: POST /api/orgs/<org_id>/roles
  • Update role: PUT /api/orgs/<org_id>/roles/
  • Find role: GET /api/orgs/<org_id>/roles/
  • Delete role: DELETE /api/orgs/<org_id>/roles/
  • Add principal to role: PUT /api/orgs/<org_id>/roles/<role_id>/principals/<principal_id>
  • Remove principal from role: DELETE /api/orgs/<org_id>/roles/<role_id>/principals/<principal_id>
  • Add group to role: PUT /api/orgs/<org_id>/roles/<role_id>/groups/<group_id>
  • Remove group from role: DELETE /api/orgs/<org_id>/roles/<role_id>/groups/<group_id>

Principals

  • Query all principals: GET /api/orgs/<org_id>/principals
  • Create principal: POST /api/orgs/<org_id>/principals
  • Update principal: PUT /api/orgs/<org_id>/principals/
  • Find principal: GET /api/orgs/<org_id>/principals/
  • Delete principal: DELETE /api/orgs/<org_id>/principals/

License Polcies

  • Query license policies: GET /api/orgs/<org_id>/licenses
  • Create license policy: POST /api/orgs/<org_id>/licenses
  • Update license policy: PUT /api/orgs/<org_id>/licenses/
  • Find license policy: GET /api/orgs/<org_id>/licenses/
  • Delete license policy: DELETE /api/orgs/<org_id>/licenses/

Checking Permission

  • GET /api/security?resource=XXX&action=XXXX&scope=XXXX

Note: See python examples for API tests, e.g.

class BankingTest(base_test.BaseTest):
    def setUp(self):
        super(BankingTest, self).setUp()
        self._realm = self.post('/api/realms', {"id":"banking"})
        self._org = self.post('/api/orgs', {"name":"bank-of-flakes", "url":"https://flakes.banky"})
        # Creating Users
        self._tom = self.post('/api/orgs/%s/principals' % self._org["id"], {"username":"tom", "organization_id":self._org["id"]})
        self._cassy = self.post('/api/orgs/%s/principals' % self._org["id"], {"username":"cassy", "organization_id":self._org["id"]})
        self._ali = self.post('/api/orgs/%s/principals' % self._org["id"], {"username":"ali", "organization_id":self._org["id"]})
        self._mike = self.post('/api/orgs/%s/principals' % self._org["id"], {"username":"mike", "organization_id":self._org["id"]})
        self._larry= self.post('/api/orgs/%s/principals' % self._org["id"], {"username":"larry", "organization_id":self._org["id"]})
        # Creating Roles
        self._employee = self.post('/api/orgs/%s/roles' % self._org["id"], {"name":"employee", "organization_id":self._org["id"], "realm_id":self._realm["id"]})
        self._teller = self.post('/api/orgs/%s/roles' % self._org["id"], {"name":"teller", "organization_id":self._org["id"], "realm_id":self._realm["id"], "parent_id": self._employee["id"]})
        self._csr = self.post('/api/orgs/%s/roles' % self._org["id"], {"name":"csr", "organization_id":self._org["id"], "realm_id":self._realm["id"], "parent_id": self._teller["id"]})
        self._accountant = self.post('/api/orgs/%s/roles' % self._org["id"], {"name":"accountant", "organization_id":self._org["id"], "realm_id":self._realm["id"], "parent_id": self._employee["id"]})
        self._accountant_manager = self.post('/api/orgs/%s/roles' % self._org["id"], {"name":"accountant_manager", "organization_id":self._org["id"], "realm_id":self._realm["id"], "parent_id": self._accountant["id"]})
        self._loan_officer = self.post('/api/orgs/%s/roles' % self._org["id"], {"name":"loan_officer", "organization_id":self._org["id"], "realm_id":self._realm["id"], "parent_id": self._accountant_manager["id"]})
        # Creating Resources
        self._deposit_account = self.post('/api/realms/%s/resources' % self._realm["id"], {"resource_name":"DepositAccount", "realm_id":self._realm["id"]})
        self._loan_account = self.post('/api/realms/%s/resources' % self._realm["id"], {"resource_name":"LoanAccount", "realm_id":self._realm["id"]})
        self._general_ledger = self.post('/api/realms/%s/resources' % self._realm["id"], {"resource_name":"GeneralLedger", "realm_id":self._realm["id"]})
        self._posting_rules = self.post('/api/realms/%s/resources' % self._realm["id"], {"resource_name":"GeneralLedgerPostingRules", "realm_id":self._realm["id"]})

        # Creating claims for resources
        self._cd_deposit = self.post('/api/realms/%s/resources/%s/claims' % (self._realm["id"], self._deposit_account["id"]), {"action":"(CREATE|DELETE)", "realm_id":self._realm["id"]})
        self._ru_deposit = self.post('/api/realms/%s/resources/%s/claims' % (self._realm["id"], self._deposit_account["id"]), {"action":"(READ|UPDATE)", "realm_id":self._realm["id"]})
        self._cd_loan = self.post('/api/realms/%s/resources/%s/claims' % (self._realm["id"], self._loan_account["id"]), {"action":"(CREATE|DELETE)", "realm_id":self._realm["id"]})
        self._ru_loan = self.post('/api/realms/%s/resources/%s/claims' % (self._realm["id"], self._loan_account["id"]), {"action":"(READ|UPDATE)", "realm_id":self._realm["id"]})
        self._rd_ledger = self.post('/api/realms/%s/resources/%s/claims' % (self._realm["id"], self._general_ledger["id"]), {"action":"(READ|DELETE|CREATE)", "realm_id":self._realm["id"]})
        self._r_glpr = self.post('/api/realms/%s/resources/%s/claims' % (self._realm["id"], self._general_ledger["id"]), {"action":"(READ)", "realm_id":self._realm["id"]})
        self._cud_glpr = self.post('/api/realms/%s/resources/%s/claims' % (self._realm["id"], self._posting_rules["id"]), {"action":"(CREATE|UPDATE|DELETE)", "realm_id":self._realm["id"]})

        # Mapping Principals to Roles
        self.put('/api/orgs/%s/roles/%s/principals/%s?max=10&constraints=&expired_at=%s' % (self._org["id"], self._teller["id"], self._tom["id"], urllib.quote('2033-6-17T00:00:00+05:30', safe='')), {})
        self.put('/api/orgs/%s/roles/%s/principals/%s?max=10&constraints=&expired_at=%s' % (self._org["id"], self._csr["id"], self._cassy["id"], urllib.quote('2033-6-17T00:00:00+05:30', safe='')), {})
        self.put('/api/orgs/%s/roles/%s/principals/%s?max=10&constraints=&expired_at=%s' % (self._org["id"], self._accountant["id"], self._ali["id"], urllib.quote('2033-6-17T00:00:00+05:30', safe='')), {})
        self.put('/api/orgs/%s/roles/%s/principals/%s?max=10&constraints=&expired_at=%s' % (self._org["id"], self._accountant_manager["id"], self._mike["id"], urllib.quote('2033-6-17T00:00:00+05:30', safe='')), {})
        self.put('/api/orgs/%s/roles/%s/principals/%s?max=10&constraints=&expired_at=%s' % (self._org["id"], self._loan_officer["id"], self._larry["id"], urllib.quote('2033-6-17T00:00:00+05:30', safe='')), {})

        # Map claims to roles as follows:
        self.put('/api/realms/%s/resources/%s/claims/%s/roles/%s' % (self._realm["id"], self._ru_deposit["resource_id"], self._ru_deposit["id"], self._teller["id"]), {})
        self.put('/api/realms/%s/resources/%s/claims/%s/roles/%s' % (self._realm["id"], self._cd_deposit["resource_id"], self._cd_deposit["id"], self._csr["id"]), {})
        self.put('/api/realms/%s/resources/%s/claims/%s/roles/%s' % (self._realm["id"], self._rd_ledger["resource_id"], self._rd_ledger["id"], self._accountant["id"]), {})
        self.put('/api/realms/%s/resources/%s/claims/%s/roles/%s' % (self._realm["id"], self._ru_loan["resource_id"], self._ru_loan["id"], self._accountant["id"]), {})
        self.put('/api/realms/%s/resources/%s/claims/%s/roles/%s' % (self._realm["id"], self._cd_loan["resource_id"], self._cd_loan["id"], self._accountant_manager["id"]), {})
        self.put('/api/realms/%s/resources/%s/claims/%s/roles/%s' % (self._realm["id"], self._r_glpr["resource_id"], self._r_glpr["id"], self._accountant_manager["id"]), {})
        self.put('/api/realms/%s/resources/%s/claims/%s/roles/%s' % (self._realm["id"], self._cud_glpr["resource_id"], self._cud_glpr["id"], self._loan_officer["id"]), {})

    def test_tom_teller_may_read_deposit_account(self):
        self._principal = self._tom
        resp = self.get('/api/security?resource=DepositAccount&action=READ')
        self.assertEquals("Allow", resp)

    def test_ali_accountant_may_not_read_deposit_account(self):
        self._principal = self._ali
        try:
            resp = self.get('/api/security?resource=DepositAccount&action=READ')
            self.assertTrue(False)
        except Exception as e:
            None

    def test_tom_teller_may_not_delete_deposit_account(self):
        self._principal = self._tom
        try:
            resp = self.get('/api/security?resource=DepositAccount&action=DELETE')
            self.assertTrue(False)
        except Exception as e:
            None

    def test_cassy_csr_may_delete_deposit_account(self):
        self._principal = self._cassy
        resp = self.get('/api/security?resource=DepositAccount&action=DELETE')
        self.assertEquals("Allow", resp)

    def test_ali_accountant_may_read_general_ledger(self):
        self._principal = self._ali
        resp = self.get('/api/security?resource=GeneralLedger&action=READ')
        self.assertEquals("Allow", resp)

    def test_ali_accountant_may_not_delete_general_ledger(self):
        self._principal = self._ali
        try:
            resp = self.get('/api/security?resource=GeneralLedger&action=DELETE')
            self.assertTrue(False)
        except Exception as e:
            None

    def test_mike_accountant_manager_may_delete_general_ledger(self):
        self._principal = self._mike
        resp = self.get('/api/security?resource=GeneralLedger&action=DELETE')
        self.assertEquals("Allow", resp)

    def test_mike_accountant_manager_may_not_post_rules(self):
        self._principal = self._mike
        try:
            resp = self.get('/api/security?resource=GeneralLedgerPostingRules&action=CREATE')
            self.assertTrue(False)
        except Exception as e:
            None

    def test_larry_loan_officerr_may_post_rules(self):
        self._principal = self._larry
        resp = self.get('/api/security?resource=GeneralLedger&action=DELETE')
        self.assertEquals("Allow", resp)

Contact

Please send questions or suggestions to bhatti AT plexobject.com.

References