Skip to content

Commit

Permalink
Decentralized authorization app (#240)
Browse files Browse the repository at this point in the history
* Decentralized authorization app

* Fixed the readme.md

* Added role and user tables

* Added role and user tables

* Updated the route to check if a user is allowed an action

* Fixed review comments from Amaury

* Updated README.md

---------

Co-authored-by: Yagnesh Setti <[email protected]>
  • Loading branch information
msftsettiy and settiy-ms authored Oct 11, 2023
1 parent 8c021e9 commit 1a999c0
Show file tree
Hide file tree
Showing 27 changed files with 1,392 additions and 0 deletions.
48 changes: 48 additions & 0 deletions decentralize-rbac-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Decentralized AuthZ application

This is the _CCF Decentralized AuthZ app - sample_ in typescript.

## Overview

The CCF network will be used to host a decentralized RBAC application where a consortium of members from different organizations would manage the roles, allowed action for a role and users. A user would have a specific role that would determine the allowed action.

A service could use the decentralized RBAC application to determine if an action is allowed for a logged-in user.

## Architecture

The application consists of two parts: Role and User Management, Authorization.

- Role and User Management
- API Endpoint: allow members to add a role and action allowed for a role.
- API Endpoint: allow members to add a user and their role.
- Authorization
- Check if a user exist and an action is allowed.

### Repository Layout

```text
📂
└── src Application source code
| └── auth Member and User cert Authentication
│ └── endpoints Application endpoints
│ └── repositories Data repositories
│ └── services Domain services
│ └── utils utility classes
```

## Getting Started

To get started and run the application locally, start with setting up the environment.

```bash
# setup the environment
git clone https://github.com/microsoft/ccf-app-samples # Clone the samples repository
code ccf-app-samples # open samples repository in Visual studio code

# In the VScode terminal window
cd decentralized-authz-app # Navigate to app folder
npm run build # Build and create the application deployment bundle
```

## Local Deployment
13 changes: 13 additions & 0 deletions decentralize-rbac-app/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
],
"@babel/preset-typescript"
]
}
67 changes: 67 additions & 0 deletions decentralize-rbac-app/build_bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { readdirSync, statSync, readFileSync, writeFileSync } from "fs";
import { join, posix, sep } from "path";

const args = process.argv.slice(2);

const getAllFiles = function (dirPath, arrayOfFiles) {
arrayOfFiles = arrayOfFiles || [];

const files = readdirSync(dirPath);
for (const file of files) {
const filePath = join(dirPath, file);
if (statSync(filePath).isDirectory()) {
arrayOfFiles = getAllFiles(filePath, arrayOfFiles);
} else {
arrayOfFiles.push(filePath);
}
}

return arrayOfFiles;
};

const removePrefix = function (s, prefix) {
return s.substr(prefix.length).split(sep).join(posix.sep);
};

const rootDir = args[0];

const metadataPath = join(rootDir, "app.json");
const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));

const srcDir = join(rootDir, "src");
const allFiles = getAllFiles(srcDir);

// The trailing / is included so that it is trimmed in removePrefix.
// This produces "foo/bar.js" rather than "/foo/bar.js"
const toTrim = srcDir + "/";

const modules = allFiles.map(function (filePath) {
return {
name: removePrefix(filePath, toTrim),
module: readFileSync(filePath, "utf-8"),
};
});

const bundlePath = join(args[0], "bundle.json");
const appRegPath = join(args[0], "set_js_app.json");
const bundle = {
metadata: metadata,
modules: modules,
};
const app_reg = {
actions: [
{
name: "set_js_app",
args: {
bundle: bundle,
disable_bytecode_cache: false,
},
},
],
};

console.log(
`Writing bundle containing ${modules.length} modules to ${bundlePath}`
);
writeFileSync(bundlePath, JSON.stringify(bundle));
writeFileSync(appRegPath, JSON.stringify(app_reg));
57 changes: 57 additions & 0 deletions decentralize-rbac-app/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
* https://jestjs.io/docs/ecmascript-modules
* https://microsoft.github.io/CCF/main/js/ccf-app/modules/polyfill.html
* To Run: Add to package.json > "scripts": {"unit-test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"}
*/

export default {
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,

// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: ["./src/**/*.ts"],

// The directory where Jest should output its coverage files
coverageDirectory: "test/coverage",

// An array of file extensions your modules use
moduleFileExtensions: [
"js",
"mjs",
"cjs",
"jsx",
"ts",
"tsx",
"json",
"node",
],

// A preset that is used as a base for Jest's configuration
// preset: undefined,
// preset: 'ts-jest',

// A list of paths to directories that Jest should use to search for files in
roots: ["./"],

// The test environment that will be used for testing
testEnvironment: "node",

testMatch: ["**/test/unit-test/**/*.test.(ts|js|mjs)"],

// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
testPathIgnorePatterns: ["/node_modules/", "/lib/"],

// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",

// A map from regular expressions to paths to transformers
// transform: undefined,
transform: { "^.+\\.[t|j]sx?$": "babel-jest" },

extensionsToTreatAsEsm: [".ts"],

// Indicates whether each individual test should be reported during the run
verbose: true,
};
66 changes: 66 additions & 0 deletions decentralize-rbac-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"private": true,
"scripts": {
"build": "del-cli -f dist/ && rollup --config && cp src/endpoints/app.json dist/ && node build_bundle.js dist/",
"bundle": "node build_bundle.js dist",
"unit-test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"create-jwt-config": "ts-node --esm ./test/utils/jwt-config-generator.ts",
"e2e-test": "ts-node --esm ./test/e2e-test/src/index.ts"
},
"type": "module",
"engines": {
"node": ">=14"
},
"dependencies": {
"@microsoft/ccf-app": "^4.0.7",
"axios": "^1.2.4",
"crypto-js": "^3.1.9-1",
"inquirer": "9.1.4",
"js-base64": "^3.5.2",
"jsonwebtoken": "^9.0.0",
"jsrsasign": "^10.0.4",
"jsrsasign-util": "^1.0.2",
"jwt-decode": "^3.0.0",
"lodash-es": "^4.17.15",
"node-forge": "^1.3.1",
"protobufjs": "^7.2.4"
},
"devDependencies": {
"@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.18.6",
"@jest/globals": "^29.3.1",
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-node-resolve": "^11.2.0",
"@rollup/plugin-typescript": "^8.2.0",
"@types/inquirer": "^9.0.3",
"@types/jasmine": "^4.3.0",
"@types/jest": "^29.2.4",
"@types/jsrsasign": "^8.0.7",
"@types/lodash-es": "^4.17.3",
"@types/mocha": "^10.0.0",
"@types/node": "^18.11.9",
"axios": "^1.2.2",
"babel-jest": "^29.3.1",
"del-cli": "^3.0.1",
"http-server": "^0.13.0",
"jest": "^29.3.1",
"rollup": "^2.41.0",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"tslib": "^2.0.1",
"typescript": "^4.9.4",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard-with-typescript": "^4.0.0",
"eslint-plugin-import": "^2.27.4",
"eslint-plugin-n": "^15.5.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-unused-imports": "^2.0.0",
"prettier": "^2.8.0",
"rimraf": "^3.0.2"
}
}
14 changes: 14 additions & 0 deletions decentralize-rbac-app/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";

export default {
input: "src/endpoints/all.ts",
output: {
dir: "dist/src",
format: "es",
preserveModules: true,
preserveModulesRoot: "src",
},
plugins: [nodeResolve(), typescript(), commonjs()],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as ccfapp from "@microsoft/ccf-app";
import { ccf } from "@microsoft/ccf-app/global";
import { ServiceResult } from "../../../utils/service-result";
import { IValidatorService } from "../validation-service";
import { UserMemberAuthnIdentity } from "./user-cert-validation";

/**
* CCF member information
* https://microsoft.github.io/CCF/main/audit/builtin_maps.html#members-info
*/
enum MemberStatus{
ACCEPTED = "Accepted",
ACTIVE = "Active"
}

interface CCFMember {
status: MemberStatus;
}

export class MemberCertValidator implements IValidatorService {
validate(request: ccfapp.Request<any>): ServiceResult<string> {
const memberCaller = request.caller as unknown as UserMemberAuthnIdentity;
const identityId = memberCaller.id;
const isValid = this.isActiveMember(identityId);
if (isValid.success && isValid.content) {
return ServiceResult.Succeeded(identityId);
}
return ServiceResult.Failed({
errorMessage: "Error: invalid caller identity",
errorType: "AuthenticationError",
});
}

/**
* Checks if a member exists and active
* @see https://microsoft.github.io/CCF/main/audit/builtin_maps.html#members-info
* @param {string} memberId memberId to check if it exists and active
* @returns {ServiceResult<boolean>}
*/
public isActiveMember(memberId: string): ServiceResult<boolean> {
const membersCerts = ccfapp.typedKv(
"public:ccf.gov.members.certs",
ccfapp.string,
ccfapp.arrayBuffer
);

const isMember = membersCerts.has(memberId);

const membersInfo = ccfapp.typedKv(
"public:ccf.gov.members.info",
ccfapp.string,
ccfapp.json<CCFMember>()
);

const memberInfo = membersInfo.get(memberId);

const isActiveMember = memberInfo && memberInfo.status === MemberStatus.ACTIVE;

return ServiceResult.Succeeded(isActiveMember && isMember);
}
}

/**
* Export the member cert validator
*/
const memberCertValidator: IValidatorService = new MemberCertValidator();
export default memberCertValidator;
Loading

0 comments on commit 1a999c0

Please sign in to comment.