From 53d3f330665187cdb640f645d924d0f5af2522d4 Mon Sep 17 00:00:00 2001 From: christophercr Date: Mon, 18 Mar 2019 17:58:42 +0100 Subject: [PATCH] feat(stark-rbac): implement new Stark-RBAC package including StarkRBACAuthorization module. Add demos for RBAC features in Showcase ISSUES CLOSED: #105 --- build-functions.sh | 4 +- build.sh | 2 +- combine-packages-coverage.js | 6 +- gh-deploy.sh | 8 + greenkeeper.json | 14 +- modules.txt | 1 + package.json | 29 +- packages/stark-rbac/.compodocrc.json | 6 + packages/stark-rbac/.npmrc | 1 + packages/stark-rbac/README.md | 16 + packages/stark-rbac/angular.json | 30 + packages/stark-rbac/base.spec.ts | 41 ++ packages/stark-rbac/index.ts | 6 + packages/stark-rbac/karma.conf.ci.js | 36 ++ packages/stark-rbac/karma.conf.js | 64 +++ packages/stark-rbac/package-lock.json | 33 ++ packages/stark-rbac/package.json | 56 ++ packages/stark-rbac/public_api.ts | 6 + packages/stark-rbac/rollup.config.js | 22 + packages/stark-rbac/src/modules.ts | 1 + .../stark-rbac/src/modules/authorization.ts | 5 + .../src/modules/authorization/actions.ts | 1 + .../actions/authorization.actions.ts | 46 ++ .../authorization/authorization.module.ts | 58 ++ .../src/modules/authorization/directives.ts | 3 + .../hide-on-permission.directive.spec.ts | 98 ++++ .../hide-on-permission.directive.ts | 87 +++ .../directives/permission.intf.ts | 9 + .../show-on-permission.directive.spec.ts | 98 ++++ .../show-on-permission.directive.ts | 87 +++ .../src/modules/authorization/entities.ts | 2 + .../entities/state-permissions.entity.intf.ts | 67 +++ .../entities/state-redirection.intf.ts | 21 + .../src/modules/authorization/services.ts | 2 + .../services/authorization.service.intf.ts | 40 ++ .../services/authorization.service.spec.ts | 541 ++++++++++++++++++ .../services/authorization.service.ts | 166 ++++++ .../src/modules/authorization/testing.ts | 1 + .../testing/authorization.mock.ts | 29 + packages/stark-rbac/src/stark-rbac.ts | 2 + packages/stark-rbac/testing/index.ts | 6 + packages/stark-rbac/testing/package.json | 15 + packages/stark-rbac/testing/public_api.ts | 6 + packages/stark-rbac/testing/rollup.config.js | 22 + .../stark-rbac/testing/tsconfig-build.json | 45 ++ packages/stark-rbac/tsconfig-build.json | 50 ++ packages/stark-rbac/tsconfig.spec.json | 19 + packages/stark-rbac/tslint.json | 31 + packages/stark-ui/README.md | 3 + packages/stark-ui/public_api.ts | 2 - packages/tsconfig.json | 1 + showcase/package-lock.json | 25 +- showcase/package.json | 9 +- showcase/src/app/app-menu.config.ts | 39 +- showcase/src/app/app.component.html | 14 +- showcase/src/app/app.module.ts | 7 +- showcase/src/app/app.routes.ts | 5 + .../src/app/demo-rbac/demo-rbac.module.ts | 32 ++ showcase/src/app/demo-rbac/index.ts | 2 + ...thorization-directives-page.component.html | 48 ++ ...thorization-directives-page.component.scss | 13 + ...authorization-directives-page.component.ts | 45 ++ .../pages/authorization-directives/index.ts | 1 + ...-authorization-service-page.component.html | 33 ++ ...-authorization-service-page.component.scss | 8 + ...mo-authorization-service-page.component.ts | 68 +++ .../pages/authorization-service/index.ts | 1 + showcase/src/app/demo-rbac/pages/index.ts | 3 + .../demo-protected-page.component.html | 7 + .../demo-protected-page.component.ts | 7 + .../demo-rbac/pages/protected-page/index.ts | 1 + showcase/src/app/demo-rbac/routes.ts | 59 ++ .../services/demo-authorization.service.ts | 24 + showcase/src/app/shared/effects/index.ts | 1 + ...rk-rbac-unauthorized-navigation.effects.ts | 82 +++ .../getting-started-page.component.html | 5 + .../welcome/pages/home/_home-page-theme.scss | 34 +- .../pages/home/_home-page.component.scss | 81 ++- .../pages/home/home-page.component.html | 101 +++- .../welcome/pages/news/_news-page-theme.scss | 4 + .../pages/news/_news-page.component.scss | 95 ++- .../pages/news/news-page.component.html | 56 +- .../hide-on-permission.html | 12 + .../hide-on-permission.ts | 22 + .../show-on-permission.html | 12 + .../show-on-permission.ts | 22 + .../authorization-service.html | 3 + showcase/src/assets/translations/en.json | 44 +- showcase/src/assets/translations/fr.json | 42 +- showcase/src/assets/translations/nl.json | 42 +- showcase/src/styles/_theme.scss | 5 +- 91 files changed, 2846 insertions(+), 213 deletions(-) create mode 100644 packages/stark-rbac/.compodocrc.json create mode 100644 packages/stark-rbac/.npmrc create mode 100644 packages/stark-rbac/README.md create mode 100644 packages/stark-rbac/angular.json create mode 100644 packages/stark-rbac/base.spec.ts create mode 100644 packages/stark-rbac/index.ts create mode 100644 packages/stark-rbac/karma.conf.ci.js create mode 100644 packages/stark-rbac/karma.conf.js create mode 100644 packages/stark-rbac/package-lock.json create mode 100644 packages/stark-rbac/package.json create mode 100644 packages/stark-rbac/public_api.ts create mode 100644 packages/stark-rbac/rollup.config.js create mode 100644 packages/stark-rbac/src/modules.ts create mode 100644 packages/stark-rbac/src/modules/authorization.ts create mode 100644 packages/stark-rbac/src/modules/authorization/actions.ts create mode 100644 packages/stark-rbac/src/modules/authorization/actions/authorization.actions.ts create mode 100644 packages/stark-rbac/src/modules/authorization/authorization.module.ts create mode 100644 packages/stark-rbac/src/modules/authorization/directives.ts create mode 100644 packages/stark-rbac/src/modules/authorization/directives/hide-on-permission.directive.spec.ts create mode 100644 packages/stark-rbac/src/modules/authorization/directives/hide-on-permission.directive.ts create mode 100644 packages/stark-rbac/src/modules/authorization/directives/permission.intf.ts create mode 100644 packages/stark-rbac/src/modules/authorization/directives/show-on-permission.directive.spec.ts create mode 100644 packages/stark-rbac/src/modules/authorization/directives/show-on-permission.directive.ts create mode 100644 packages/stark-rbac/src/modules/authorization/entities.ts create mode 100644 packages/stark-rbac/src/modules/authorization/entities/state-permissions.entity.intf.ts create mode 100644 packages/stark-rbac/src/modules/authorization/entities/state-redirection.intf.ts create mode 100644 packages/stark-rbac/src/modules/authorization/services.ts create mode 100644 packages/stark-rbac/src/modules/authorization/services/authorization.service.intf.ts create mode 100644 packages/stark-rbac/src/modules/authorization/services/authorization.service.spec.ts create mode 100644 packages/stark-rbac/src/modules/authorization/services/authorization.service.ts create mode 100644 packages/stark-rbac/src/modules/authorization/testing.ts create mode 100644 packages/stark-rbac/src/modules/authorization/testing/authorization.mock.ts create mode 100644 packages/stark-rbac/src/stark-rbac.ts create mode 100644 packages/stark-rbac/testing/index.ts create mode 100644 packages/stark-rbac/testing/package.json create mode 100644 packages/stark-rbac/testing/public_api.ts create mode 100644 packages/stark-rbac/testing/rollup.config.js create mode 100644 packages/stark-rbac/testing/tsconfig-build.json create mode 100644 packages/stark-rbac/tsconfig-build.json create mode 100644 packages/stark-rbac/tsconfig.spec.json create mode 100644 packages/stark-rbac/tslint.json create mode 100644 showcase/src/app/demo-rbac/demo-rbac.module.ts create mode 100644 showcase/src/app/demo-rbac/index.ts create mode 100644 showcase/src/app/demo-rbac/pages/authorization-directives/demo-authorization-directives-page.component.html create mode 100644 showcase/src/app/demo-rbac/pages/authorization-directives/demo-authorization-directives-page.component.scss create mode 100644 showcase/src/app/demo-rbac/pages/authorization-directives/demo-authorization-directives-page.component.ts create mode 100644 showcase/src/app/demo-rbac/pages/authorization-directives/index.ts create mode 100644 showcase/src/app/demo-rbac/pages/authorization-service/demo-authorization-service-page.component.html create mode 100644 showcase/src/app/demo-rbac/pages/authorization-service/demo-authorization-service-page.component.scss create mode 100644 showcase/src/app/demo-rbac/pages/authorization-service/demo-authorization-service-page.component.ts create mode 100644 showcase/src/app/demo-rbac/pages/authorization-service/index.ts create mode 100644 showcase/src/app/demo-rbac/pages/index.ts create mode 100644 showcase/src/app/demo-rbac/pages/protected-page/demo-protected-page.component.html create mode 100644 showcase/src/app/demo-rbac/pages/protected-page/demo-protected-page.component.ts create mode 100644 showcase/src/app/demo-rbac/pages/protected-page/index.ts create mode 100644 showcase/src/app/demo-rbac/routes.ts create mode 100644 showcase/src/app/demo-rbac/services/demo-authorization.service.ts create mode 100644 showcase/src/app/shared/effects/stark-rbac-unauthorized-navigation.effects.ts create mode 100644 showcase/src/app/welcome/pages/news/_news-page-theme.scss create mode 100644 showcase/src/assets/examples/rbac-authorization-directives/hide-on-permission.html create mode 100644 showcase/src/assets/examples/rbac-authorization-directives/hide-on-permission.ts create mode 100644 showcase/src/assets/examples/rbac-authorization-directives/show-on-permission.html create mode 100644 showcase/src/assets/examples/rbac-authorization-directives/show-on-permission.ts create mode 100644 showcase/src/assets/examples/rbac-authorization-service/authorization-service.html diff --git a/build-functions.sh b/build-functions.sh index f8622f638a..b2ba433141 100644 --- a/build-functions.sh +++ b/build-functions.sh @@ -463,8 +463,8 @@ logTrace "Executing function: ${FUNCNAME[0]}" 1 logTrace "SHA-512: $SHA" logTrace "SHA-512 escaped: $ESCAPED_SHA" - local PATTERN="\\\"\@nationalbankbelgium\/$PACKAGE\\\": \\{(\s*)\\\"version\\\": \\\"(\S*)\\\"(,(\s*)\\\"resolved\\\": \\\"(.*))?,(\s*)\\\"integrity\\\": \\\"sha512-(.*)\\\"," - local REPLACEMENT='"\@nationalbankbelgium\/'$PACKAGE'": {$1"version": "'$TGZ_PATH'",$4"integrity": "sha512-'$ESCAPED_SHA'",' + local PATTERN="\\\"\@nationalbankbelgium\/$PACKAGE\\\": \\{(\s*)\\\"version\\\": \\\"(\S*)\\\"(,(\s*)\\\"resolved\\\": \\\"(.*))?,(\s*)\\\"integrity\\\": \\\"sha512-(.*)\\\"" + local REPLACEMENT='"\@nationalbankbelgium\/'$PACKAGE'": {$1"version": "'$TGZ_PATH'",$4"integrity": "sha512-'$ESCAPED_SHA'"' logTrace "PATTERN: $PATTERN" logTrace "REPLACEMENT: $REPLACEMENT" diff --git a/build.sh b/build.sh index f287ccf3f5..844c3d7452 100644 --- a/build.sh +++ b/build.sh @@ -20,7 +20,7 @@ cd ${currentDir} # List of all packages, separated by a space # Packages will be transpiled using NGC (unless if also part of NODE_PACKAGES like the build package) -PACKAGES=(stark-core stark-ui) +PACKAGES=(stark-core stark-ui stark-rbac) # Packages that should not be compiled by NGC but just with TSC TSC_PACKAGES=() diff --git a/combine-packages-coverage.js b/combine-packages-coverage.js index 604c455e53..a17e2a4a94 100644 --- a/combine-packages-coverage.js +++ b/combine-packages-coverage.js @@ -2,7 +2,11 @@ const fs = require("fs"); const StreamConcat = require("stream-concat"); // add the reports of all the different Stark packages to be combined -const fileNames = ["packages/stark-core/reports/coverage/packages/lcov.info", "packages/stark-ui/reports/coverage/packages/lcov.info"]; +const fileNames = [ + "packages/stark-core/reports/coverage/packages/lcov.info", + "packages/stark-ui/reports/coverage/packages/lcov.info", + "packages/stark-rbac/reports/coverage/packages/lcov.info" +]; let fileIndex = 0; const nextStream = function() { diff --git a/gh-deploy.sh b/gh-deploy.sh index b5a78832bf..01d088fdd8 100644 --- a/gh-deploy.sh +++ b/gh-deploy.sh @@ -28,6 +28,7 @@ SSH_KEY_CLEARTEXT_FILE="stark-ssh" STARK_CORE="stark-core" STARK_UI="stark-ui" +STARK_RBAC="stark-rbac" SHOWCASE="showcase" API_DOCS_DIR_NAME="api-docs" LATEST_DIR_NAME="latest" @@ -287,6 +288,9 @@ API_DOCS_TARGET_DIR_STARK_CORE_LATEST=${DOCS_WORK_DIR}/${API_DOCS_DIR_NAME}/${ST API_DOCS_TARGET_DIR_STARK_UI=${DOCS_WORK_DIR}/${API_DOCS_DIR_NAME}/${STARK_UI}/${DOCS_VERSION} API_DOCS_TARGET_DIR_STARK_UI_LATEST=${DOCS_WORK_DIR}/${API_DOCS_DIR_NAME}/${STARK_UI}/${LATEST_DIR_NAME} +API_DOCS_TARGET_DIR_STARK_RBAC=${DOCS_WORK_DIR}/${API_DOCS_DIR_NAME}/${STARK_RBAC}/${DOCS_VERSION} +API_DOCS_TARGET_DIR_STARK_RBAC_LATEST=${DOCS_WORK_DIR}/${API_DOCS_DIR_NAME}/${STARK_RBAC}/${LATEST_DIR_NAME} + SHOWCASE_TARGET_DIR=${DOCS_WORK_DIR}/${SHOWCASE}/${DOCS_VERSION} SHOWCASE_TARGET_DIR_LATEST=${DOCS_WORK_DIR}/${SHOWCASE}/${LATEST_DIR_NAME} @@ -312,6 +316,10 @@ logTrace "Copying ${STARK_UI} API docs" syncFiles ${API_DOCS_SOURCE_DIR}/${STARK_UI} ${API_DOCS_TARGET_DIR_STARK_UI} "${syncOptions[@]}" syncFiles ${API_DOCS_SOURCE_DIR}/${STARK_UI} ${API_DOCS_TARGET_DIR_STARK_UI_LATEST} "${syncOptions[@]}" +logTrace "Copying ${STARK_RBAC} API docs" +syncFiles ${API_DOCS_SOURCE_DIR}/${STARK_RBAC} ${API_DOCS_TARGET_DIR_STARK_RBAC} "${syncOptions[@]}" +syncFiles ${API_DOCS_SOURCE_DIR}/${STARK_RBAC} ${API_DOCS_TARGET_DIR_STARK_RBAC_LATEST} "${syncOptions[@]}" + logTrace "Copying ${SHOWCASE}" NODE_REPLACE_URLS="node ${PROJECT_ROOT_DIR}/${SHOWCASE}/ghpages-adapt-bundle-urls.js" diff --git a/greenkeeper.json b/greenkeeper.json index b504a48a81..6e828c90d6 100644 --- a/greenkeeper.json +++ b/greenkeeper.json @@ -5,22 +5,14 @@ "package.json", "packages/stark-build/package.json", "packages/stark-core/package.json", + "packages/stark-rbac/package.json", "packages/stark-testing/package.json", "packages/stark-ui/package.json" ] }, "stark-apps": { - "packages": [ - "showcase/package.json", - "starter/package.json" - ] + "packages": ["showcase/package.json", "starter/package.json"] } }, - "ignore": [ - "@angular/flex-layout", - "@compodoc/compodoc", - "@types/node", - "class-validator", - "typescript" - ] + "ignore": ["@angular/flex-layout", "@compodoc/compodoc", "@types/node", "class-validator", "typescript"] } diff --git a/modules.txt b/modules.txt index 580575a634..7e88786141 100644 --- a/modules.txt +++ b/modules.txt @@ -2,3 +2,4 @@ stark-build stark-testing stark-core stark-ui +stark-rbac diff --git a/package.json b/package.json index fe25590be0..2d72f252fa 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "build:trace": "npm run build -- --trace", "build:stark-build": "npm run build -- --packages=stark-build", "build:stark-core": "npm run build -- --packages=stark-core", + "build:stark-rbac": "npm run build -- --packages=stark-rbac", "build:stark-testing": "npm run build -- --packages=stark-testing", "build:stark-ui": "npm run build -- --packages=stark-ui", "build:showcase": "cd showcase && npm run build:prod && cd ..", @@ -73,31 +74,36 @@ "check:starter:stark-versions": "node ./check-stark-versions.js ./starter/package.json latest", "check:nightly:stark-versions": "node ./check-nightly-version.js ./package.json", "clean": "npx rimraf ./dist", - "clean:all": "npm run clean && npm run clean:stark-build && npm run clean:stark-core && npm run clean:stark-ui && npm run clean:stark-testing && npm run clean:starter && npm run clean:showcase", + "clean:all": "npm run clean && npm run clean:stark-build && npm run clean:stark-testing && npm run clean:stark-core && npm run clean:stark-ui && npm run clean:stark-rbac && npm run clean:starter && npm run clean:showcase", "clean:stark-build": "cd packages/stark-build && npm run clean && cd ../..", "clean:stark-core": "cd packages/stark-core && npm run clean && cd ../..", + "clean:stark-rbac": "cd packages/stark-rbac && npm run clean && cd ../..", "clean:stark-testing": "cd packages/stark-testing && npm run clean && cd ../..", "clean:stark-ui": "cd packages/stark-ui && npm run clean && cd ../..", "clean:showcase": "cd showcase && npx rimraf ./dist && npx rimraf ./node_modules/@nationalbankbelgium && cd ..", "clean:slate": "npm run clean:all && npm run clean:modules:all && npm install && npm run install:all", "clean:starter": "cd starter && npx rimraf ./dist && npx rimraf ./node_modules/@nationalbankbelgium && cd ..", "clean:modules": "npx rimraf ./node_modules package-lock.json", - "clean:modules:all": "npm run clean:modules && npm run clean:modules:stark-build && npm run clean:modules:stark-core && npm run clean:modules:stark-testing && npm run clean:modules:stark-ui && npm run clean:modules:starter && npm run clean:modules:showcase", + "clean:modules:all": "npm run clean:modules && npm run clean:modules:stark-build && npm run clean:modules:stark-testing && npm run clean:modules:stark-core && npm run clean:modules:stark-ui && npm run clean:modules:stark-rbac && npm run clean:modules:starter && npm run clean:modules:showcase", "clean:modules:stark-build": "cd packages/stark-build && npm run clean:modules && cd ../..", "clean:modules:stark-core": "cd packages/stark-core && npm run clean:modules && cd ../..", + "clean:modules:stark-rbac": "cd packages/stark-rbac && npm run clean:modules && cd ../..", "clean:modules:stark-testing": "cd packages/stark-testing && npm run clean:modules && cd ../..", "clean:modules:stark-ui": "cd packages/stark-ui && npm run clean:modules dist && cd ../..", "clean:modules:showcase": "cd showcase && npm run clean:modules && cd ..", "clean:modules:starter": "cd starter && npm run clean:modules && cd ..", "commit": "./node_modules/.bin/git-cz", "docs": "npm run docs:clean && npm run docs:all", - "docs:all": "npm run docs:stark-core:generate && npm run docs:stark-ui:generate && npm run docs:starter:generate", + "docs:all": "npm run docs:stark-core:generate && npm run docs:stark-ui:generate && npm run docs:stark-rbac:generate && npm run docs:starter:generate", "docs:clean": "npx rimraf reports/api-docs", - "docs:coverage": "npm run docs:stark-core:coverage && npm run docs:stark-ui:coverage && npm run docs:starter:coverage", + "docs:coverage": "npm run docs:stark-core:coverage && npm run docs:stark-ui:coverage && npm run docs:stark-rbac:coverage && npm run docs:starter:coverage", "docs:publish": "bash ./gh-deploy.sh --trace", "docs:stark-core:coverage": "cd packages/stark-core && npm run docs:coverage && cd ../..", "docs:stark-core:generate": "cd packages/stark-core && npm run docs && cd ../..", "docs:stark-core:serve": "cd packages/stark-core && npm run docs:serve && cd ../..", + "docs:stark-rbac:coverage": "cd packages/stark-rbac && npm run docs:coverage && cd ../..", + "docs:stark-rbac:generate": "cd packages/stark-rbac && npm run docs && cd ../..", + "docs:stark-rbac:serve": "cd packages/stark-rbac && npm run docs:serve && cd ../..", "docs:stark-ui:coverage": "cd packages/stark-ui && npm run docs:coverage && cd ../..", "docs:stark-ui:generate": "cd packages/stark-ui && npm run docs && cd ../..", "docs:stark-ui:serve": "cd packages/stark-ui && npm run docs:serve && cd ../..", @@ -108,24 +114,27 @@ "generate:changelog-recent": "conventional-changelog -p angular | tail -n +3", "lint": "tslint --config ./tslint.json --project ./packages/tsconfig.json --format codeFrame", "lint:stark-core": "cd packages/stark-core && npm run lint && cd ../..", + "lint:stark-rbac": "cd packages/stark-rbac && npm run lint && cd ../..", "lint:stark-ui": "cd packages/stark-ui && npm run lint && cd ../..", "lint:showcase": "cd showcase && npm run lint && cd ..", "lint:starter": "cd starter && npm run lint && cd ..", - "lint:all": "npm run lint:stark-core && npm run lint:stark-ui && npm run lint:starter && npm run lint:showcase", - "install:all": "npm run install:stark-build && npm run install:stark-testing && npm run install:stark-core && npm run install:stark-ui && npm run build && npm run install:starter && npm run install:showcase", + "lint:all": "npm run lint:stark-core && npm run lint:stark-ui && npm run lint:stark-rbac && npm run lint:starter && npm run lint:showcase", + "install:all": "npm run install:stark-build && npm run install:stark-testing && npm run install:stark-core && npm run install:stark-ui && npm run install:stark-rbac && npm run build && npm run install:starter && npm run install:showcase", "install:stark-build": "cd packages/stark-build && npm install && cd ../..", "install:stark-core": "cd packages/stark-core && npm install && cd ../..", + "install:stark-rbac": "cd packages/stark-rbac && npm install && cd ../..", "install:stark-testing": "cd packages/stark-testing && npm install && cd ../..", "install:stark-ui": "cd packages/stark-ui && npm install && cd ../..", "install:showcase": "cd showcase && npm install && cd ..", "install:starter": "cd starter && npm install && cd ..", - "install:ci:all": "npm run install:ci:stark-build && npm run install:ci:stark-testing && npm run install:ci:stark-core && npm run install:ci:stark-ui && npm run build:trace && npm run install:starter && npm run install:ci:showcase", + "install:ci:all": "npm run install:ci:stark-build && npm run install:ci:stark-testing && npm run install:ci:stark-core && npm run install:ci:stark-ui && npm run install:ci:stark-rbac && npm run build:trace && npm run install:starter && npm run install:ci:showcase", "install:ci:stark-build": "cd packages/stark-build && npm ci && cd ../..", "install:ci:stark-core": "cd packages/stark-core && npm ci && cd ../..", + "install:ci:stark-rbac": "cd packages/stark-rbac && npm ci && cd ../..", "install:ci:stark-testing": "cd packages/stark-testing && npm ci && cd ../..", "install:ci:stark-ui": "cd packages/stark-ui && npm ci && cd ../..", "install:ci:showcase": "cd showcase && npm ci && cd ..", - "install:travis:all": "npm run install:stark-build && npm run install:stark-testing && npm run install:stark-core && npm run install:stark-ui && npm run build:trace && npm run update:starter && npm run update:showcase", + "install:travis:all": "npm run install:stark-build && npm run install:stark-testing && npm run install:stark-core && npm run install:stark-ui && npm run install:stark-rbac && npm run build:trace && npm run update:starter && npm run update:showcase", "ngc": "ngc", "prettier-check": "prettier \"**/*.{css,html,js,json,md,pcss,scss,ts,yml}\" --write --html-whitespace-sensitivity strict", "preupdate:showcase": "npm run clean:showcase", @@ -136,8 +145,9 @@ "starter": "cd starter && npm start && cd ..", "stylelint-check": "stylelint-config-prettier-check", "test": "npm run test:ci:all", - "test:all": "npm run test:stark-core && npm run test:stark-ui && npm run test:starter && npm run test:showcase", + "test:all": "npm run test:stark-core && npm run test:stark-ui && npm run test:stark-rbac && npm run test:starter && npm run test:showcase", "test:stark-core": "cd packages/stark-core && npm run test-fast && cd ../..", + "test:stark-rbac": "cd packages/stark-rbac && npm run test-fast && cd ../..", "test:stark-ui": "cd packages/stark-ui && npm run test-fast && cd ../..", "test:showcase": "cd showcase && npm run test-fast && cd ../..", "test:showcase:e2e": "cd showcase && npm run e2e && cd ../..", @@ -146,6 +156,7 @@ "test:starter:e2e": "cd starter && npm run e2e && cd ../..", "test:ci:all": "npm run test:ci:stark-core && npm run test:ci:stark-ui && npm run test:ci:starter && npm run test:ci:showcase", "test:ci:stark-core": "cd packages/stark-core && npm run test-fast:ci && cd ../..", + "test:ci:stark-rbac": "cd packages/stark-rbac && npm run test-fast:ci && cd ../..", "test:ci:stark-ui": "cd packages/stark-ui && npm run test-fast:ci && cd ../..", "test:ci:showcase": "cd showcase && npm run test-fast:ci && cd ../..", "test:ci:starter": "cd starter && npm run test-fast:ci && cd ../..", diff --git a/packages/stark-rbac/.compodocrc.json b/packages/stark-rbac/.compodocrc.json new file mode 100644 index 0000000000..fe0c0f3c89 --- /dev/null +++ b/packages/stark-rbac/.compodocrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../node_modules/@compodoc/compodoc/src/config/schema.json", + "theme": "material", + "tsconfig": "../tsconfig.json", + "output": "../../reports/api-docs/stark-rbac" +} diff --git a/packages/stark-rbac/.npmrc b/packages/stark-rbac/.npmrc new file mode 100644 index 0000000000..38f11c645a --- /dev/null +++ b/packages/stark-rbac/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org diff --git a/packages/stark-rbac/README.md b/packages/stark-rbac/README.md new file mode 100644 index 0000000000..ee57a98794 --- /dev/null +++ b/packages/stark-rbac/README.md @@ -0,0 +1,16 @@ +[![NPM version](https://img.shields.io/npm/v/@nationalbankbelgium/stark-rbac.svg)](https://www.npmjs.com/package/@nationalbankbelgium/stark-rbac) +[![npm](https://img.shields.io/npm/dm/@nationalbankbelgium/stark-rbac.svg)](https://www.npmjs.com/package/@nationalbankbelgium/stark-rbac) +[![Build Status](https://travis-ci.org/NationalBankBelgium/stark.svg?branch=master)](https://travis-ci.org/NationalBankBelgium/stark) +[![Dependency Status](https://david-dm.org/NationalBankBelgium/stark-rbac.svg)](https://david-dm.org/NationalBankBelgium/stark-rbac) +[![devDependency Status](https://david-dm.org/NationalBankBelgium/stark-rbac/dev-status.svg)](https://david-dm.org/NationalBankBelgium/stark-rbac#info=devDependencies) +[![License](https://img.shields.io/cocoapods/l/AFNetworking.svg)](LICENSE) + +# Stark RBAC + +Stark's RBAC module (aka stark-rbac) is a separate module in Stark that can be optionally included in any Stark based application in order to provide different elements +(directives, services and components) to support Role Based Access Control (RBAC) mechanism. + +The Stark-RBAC module depends on some functionalities provided by the Stark-Core module such as services. However you can use this module without Stark-Core +as long as you provide the same functionalities/services yourself. + +**[Getting Started](https://stark.nbb.be/api-docs/stark-rbac/latest/additional-documentation/getting-started.html)** diff --git a/packages/stark-rbac/angular.json b/packages/stark-rbac/angular.json new file mode 100644 index 0000000000..b6240fc530 --- /dev/null +++ b/packages/stark-rbac/angular.json @@ -0,0 +1,30 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoots": "projects", + "projects": { + "stark-rbac": { + "root": "", + "sourceRoot": "src", + "projectType": "library", + "architect": { + "build": { + "options": { + "outputPath": "dist", + "tsConfig": "tsconfig-build.json", + "assets": [] + } + }, + "test": { + "options": { + "main": "base.spec.ts", + "outputPath": "dist", + "tsConfig": "tsconfig.spec.json", + "assets": [] + } + } + } + } + }, + "defaultProject": "stark-rbac" +} diff --git a/packages/stark-rbac/base.spec.ts b/packages/stark-rbac/base.spec.ts new file mode 100644 index 0000000000..dacdf47c41 --- /dev/null +++ b/packages/stark-rbac/base.spec.ts @@ -0,0 +1,41 @@ +"use strict"; + +import "core-js/es6"; +import "core-js/es7/reflect"; +import "core-js/es7/string"; +import "core-js/stage/4"; + +// IE polyfills + +// See https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill +if (!Element.prototype.matches) { + // @ts-ignore + Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; +} + +// See: https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill +// @ts-ignore: Window.NodeList +if (window.NodeList && !NodeList.prototype.forEach) { + // @ts-ignore: forEach mismatching types + NodeList.prototype.forEach = Array.prototype.forEach; +} + +/* tslint:disable:no-import-side-effect */ +import "zone.js/dist/zone"; +import "zone.js/dist/long-stack-trace-zone"; +import "zone.js/dist/proxy"; // since zone.js 0.6.15 +import "zone.js/dist/sync-test"; +import "zone.js/dist/jasmine-patch"; // put here since zone.js 0.6.14 +import "zone.js/dist/async-test"; +import "zone.js/dist/fake-async-test"; +import "zone.js/dist/zone-patch-rxjs"; +import "zone.js/dist/zone-patch-rxjs-fake-async"; +/* tslint:enable:no-import-side-effect */ + +import { TestBed } from "@angular/core/testing"; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from "@angular/platform-browser-dynamic/testing"; + +TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// define global environment variable (used in some places in stark-ui) +global["ENV"] = "development"; diff --git a/packages/stark-rbac/index.ts b/packages/stark-rbac/index.ts new file mode 100644 index 0000000000..023feb5cf0 --- /dev/null +++ b/packages/stark-rbac/index.ts @@ -0,0 +1,6 @@ +// This file is not used to build this module. It is only used during editing +// by the TypeScript language service and during build for verification. `ngc` +// replaces this file with production index.ts when it rewrites private symbol +// names. + +export * from "./public_api"; diff --git a/packages/stark-rbac/karma.conf.ci.js b/packages/stark-rbac/karma.conf.ci.js new file mode 100644 index 0000000000..3701923292 --- /dev/null +++ b/packages/stark-rbac/karma.conf.ci.js @@ -0,0 +1,36 @@ +const helpers = require("../stark-testing/helpers"); + +/** + * Load karma config from Stark + */ +const defaultKarmaCIConfig = require("../stark-testing/karma.conf.ci.js").rawKarmaConfig; +const karmaTypescriptBundlerAliasResolution = require("./karma.conf").karmaTypescriptBundlerAliasResolution; +const karmaTypescriptFiles = require("./karma.conf").karmaTypescriptFiles; + +// start customizing the KarmaCI configuration from stark-testing +const starkRBACSpecificConfiguration = { + ...defaultKarmaCIConfig, + // change the module resolution for the KarmaTypescript bundler + karmaTypescriptConfig: { + ...defaultKarmaCIConfig.karmaTypescriptConfig, + bundlerOptions: { + ...defaultKarmaCIConfig.karmaTypescriptConfig.bundlerOptions, + ...karmaTypescriptBundlerAliasResolution, + transforms: [ + require("../stark-testing/node_modules/karma-typescript-angular2-transform"), + require("../stark-testing/node_modules/karma-typescript-es6-transform")({ + presets: [helpers.root("../stark-testing/node_modules/babel-preset-env")] // add preset in a way that the package can find it + }) + ] + } + }, + // change the path of the report so that Coveralls takes the right path to the source files + coverageIstanbulReporter: { ...defaultKarmaCIConfig.coverageIstanbulReporter, dir: helpers.root("reports/coverage/packages") }, + // add missing files due to "@nationalbankbelgium/stark-rbac" imports used in mock files of the testing sub-package + files: [...defaultKarmaCIConfig.files, ...karmaTypescriptFiles] +}; + +// export the configuration function that karma expects and simply return the stark configuration +module.exports = config => { + return config.set(starkRBACSpecificConfiguration); +}; diff --git a/packages/stark-rbac/karma.conf.js b/packages/stark-rbac/karma.conf.js new file mode 100644 index 0000000000..de32bb0e29 --- /dev/null +++ b/packages/stark-rbac/karma.conf.js @@ -0,0 +1,64 @@ +const helpers = require("../stark-testing/helpers"); + +/** + * Load karma config from Stark + */ +const defaultKarmaConfig = require("../stark-testing/karma.conf.js").rawKarmaConfig; + +// entry files of the "@nationalbankbelgium/stark-rbac" module imported in mock files +const karmaTypescriptFiles = [{ pattern: helpers.root("index.ts") }, { pattern: helpers.root("public_api.ts") }]; + +const karmaTypescriptBundlerAliasResolution = { + resolve: { + alias: { + // adapt the resolution of the stark-core module to the UMD module + "@nationalbankbelgium/stark-core": "../../dist/packages-dist/stark-core/bundles/stark-core.umd.js", + "@nationalbankbelgium/stark-core/testing": "../../dist/packages-dist/stark-core/bundles/stark-core-testing.umd.js", + // adapt the resolution of the 3rd party modules used in stark-core + "@angularclass/hmr": "../stark-core/node_modules/@angularclass/hmr/dist/index.js", + "@ng-idle/core": "../stark-core/node_modules/@ng-idle/core/bundles/core.umd.js", + "@ng-idle/keepalive": "../stark-core/node_modules/@ng-idle/keepalive/bundles/keepalive.umd.js", + "@ngrx/store": "../stark-core/node_modules/@ngrx/store/bundles/store.umd.js", + "@ngrx/effects": "../stark-core/node_modules/@ngrx/effects/bundles/effects.umd.js", + "@ngx-translate/core": "../stark-core/node_modules/@ngx-translate/core/bundles/ngx-translate-core.umd.js", + "@uirouter/angular": "../stark-core/node_modules/@uirouter/angular/_bundles/ui-router-ng2.js", + "@uirouter/core": "../stark-core/node_modules/@uirouter/core/lib/index.js", + "@uirouter/rx": "../stark-core/node_modules/@uirouter/rx/lib/index.js", + cerialize: "../stark-core/node_modules/cerialize/index.js", + "class-validator": "../stark-core/node_modules/class-validator/index.js", + "deep-freeze-strict": "../stark-core/node_modules/deep-freeze-strict/index.js", + moment: "../stark-core/node_modules/moment/moment.js", + ibantools: "../stark-core/node_modules/ibantools/build/ibantools.js" + } + } +}; + +// start customizing the KarmaCI configuration from stark-testing +const starkRBACSpecificConfiguration = { + ...defaultKarmaConfig, + // change the module resolution for the KarmaTypescript bundler + karmaTypescriptConfig: { + ...defaultKarmaConfig.karmaTypescriptConfig, + bundlerOptions: { + ...defaultKarmaConfig.karmaTypescriptConfig.bundlerOptions, + ...karmaTypescriptBundlerAliasResolution, + transforms: [ + require("../stark-testing/node_modules/karma-typescript-angular2-transform"), + require("../stark-testing/node_modules/karma-typescript-es6-transform")({ + presets: [helpers.root("../stark-testing/node_modules/babel-preset-env")] // add preset in a way that the package can find it + }) + ] + } + }, + // add missing files due to "@nationalbankbelgium/stark-rbac" imports used in mock files of the testing sub-package + files: [...defaultKarmaConfig.files, ...karmaTypescriptFiles] +}; + +// export the configuration function that karma expects and simply return the stark configuration +module.exports = { + default: function(config) { + return config.set(starkRBACSpecificConfiguration); + }, + karmaTypescriptBundlerAliasResolution: karmaTypescriptBundlerAliasResolution, + karmaTypescriptFiles: karmaTypescriptFiles +}; diff --git a/packages/stark-rbac/package-lock.json b/packages/stark-rbac/package-lock.json new file mode 100644 index 0000000000..aefb459850 --- /dev/null +++ b/packages/stark-rbac/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "@nationalbankbelgium/stark-rbac", + "version": "0.0.0-PLACEHOLDER-VERSION", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/jasmine": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.12.tgz", + "integrity": "sha512-lXvr2xFQEVQLkIhuGaR3GC1L9lMU1IxeWnAF/wNY5ZWpC4p9dgxkKkzMp7pntpAdv9pZSnYqgsBkCg32MXSZMg==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.121", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.121.tgz", + "integrity": "sha512-ORj7IBWj13iYufXt/VXrCNMbUuCTJfhzme5kx9U/UtcIPdJYuvPDUAlHlbNhz/8lKCLy9XGIZnGrqXOtQbPGoQ==" + }, + "@types/lodash-es": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.2.tgz", + "integrity": "sha512-Bpl6bh7kF9IdepJEfSd6g/E6OPq99jcIQP5g0xqv47P1FjLOP1+PENCy2vWvrzY+KwnnHwGAbUcTxbEr+WAnUw==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/node": { + "version": "8.10.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.44.tgz", + "integrity": "sha512-HY3SK7egERHGUfY8p6ztXIEQWcIPHouYhCGcLAPQin7gE2G/fALFz+epnMwcxKUS6aKqTVoAFdi+t1llQd3xcw==", + "dev": true + } + } +} diff --git a/packages/stark-rbac/package.json b/packages/stark-rbac/package.json new file mode 100644 index 0000000000..188c6cffe8 --- /dev/null +++ b/packages/stark-rbac/package.json @@ -0,0 +1,56 @@ +{ + "name": "@nationalbankbelgium/stark-rbac", + "version": "0.0.0-PLACEHOLDER-VERSION", + "module": "./fesm5/stark-rbac.js", + "es2015": "./fesm2015/stark-rbac.js", + "esm5": "./esm5/stark-rbac.js", + "esm2015": "./esm2015/stark-rbac.js", + "fesm5": "./fesm5/stark-rbac.js", + "fesm2015": "./fesm2015/stark-rbac.js", + "main": "bundles/stark-rbac.umd.js", + "types": "stark-rbac.d.ts", + "description": "Stark - RBAC", + "author": "Stark Team", + "contributors": [ + "PLACEHOLDER-CONTRIBUTORS" + ], + "license": "MIT", + "bugs": "https://github.com/nationalbankbelgium/stark/issues", + "homepage": "https://github.com/nationalbankbelgium/stark", + "repository": { + "type": "git", + "url": "https://github.com/NationalBankBelgium/stark.git" + }, + "engines": { + "node": ">=8.9.4", + "npm": ">=5.6.0" + }, + "dependencies": { + "@types/lodash-es": "^4.17.1" + }, + "devDependencies": { + "@types/jasmine": "^3.3.12", + "@types/node": "^8.10.37" + }, + "peerDependencies": { + "@nationalbankbelgium/stark-core": "0.0.0-PLACEHOLDER-VERSION" + }, + "scripts": { + "clean": "npx rimraf dist", + "clean:modules": "npx rimraf ./node_modules package-lock.json", + "clean:all": "npm run clean && npm run clean:modules", + "docs": "node ../../node_modules/@compodoc/compodoc/bin/index-cli src", + "docs:coverage": "npm run docs -- --coverageTest 85 --coverageTestThresholdFail true", + "docs:serve": "npm run docs -- --watch --serve --port 4321", + "ngc": "node ../../node_modules/@angular/compiler-cli/src/main.js -p ./tsconfig-build.json", + "lint": "npm run lint-ts && npm run lint-css", + "lint-ts": "node ../../node_modules/tslint/bin/tslint --config ./tslint.json --project ./tsconfig.spec.json --format codeFrame", + "lint-css": "node ../../node_modules/stylelint/bin/stylelint \"./(src|assets)/**/*.?(pc|sc|c|sa)ss\" --formatter \"string\"", + "test": "npm run lint && npm run test-fast", + "test:ci": "npm run lint && npm run test-fast:ci", + "test-fast": "node ../stark-testing/node_modules/karma/bin/karma start", + "test-fast:ci": "node ../stark-testing/node_modules/karma/bin/karma start karma.conf.ci.js", + "tsc": "node ../../node_modules/typescript/bin/tsc -p ./tsconfig-build.json", + "tslint": "node ../../node_modules/tslint/bin/tslint" + } +} diff --git a/packages/stark-rbac/public_api.ts b/packages/stark-rbac/public_api.ts new file mode 100644 index 0000000000..adb2dfa3c6 --- /dev/null +++ b/packages/stark-rbac/public_api.ts @@ -0,0 +1,6 @@ +/** + * Entry point for all public APIs of this package. + */ +export * from "./src/stark-rbac"; + +// This file only reexports content of the `src` folder. Keep it that way. diff --git a/packages/stark-rbac/rollup.config.js b/packages/stark-rbac/rollup.config.js new file mode 100644 index 0000000000..2fef592acb --- /dev/null +++ b/packages/stark-rbac/rollup.config.js @@ -0,0 +1,22 @@ +"use strict"; + +const commonData = require("../rollup.config.common-data.js"); // common configuration between environments + +module.exports = { + input: "../../dist/packages-dist/stark-rbac/fesm5/stark-rbac.js", + external: commonData.external, + plugins: commonData.plugins, + output: [ + { + file: "../../dist/packages-dist/stark-rbac/bundles/stark-rbac.umd.js", + globals: commonData.output.globals, + format: commonData.output.format, + exports: commonData.output.exports, + name: "stark.rbac", + sourcemap: commonData.output.sourcemap, + amd: { + id: "@nationalbankbelgium/stark-rbac" + } + } + ] +}; diff --git a/packages/stark-rbac/src/modules.ts b/packages/stark-rbac/src/modules.ts new file mode 100644 index 0000000000..f80aa4a682 --- /dev/null +++ b/packages/stark-rbac/src/modules.ts @@ -0,0 +1 @@ +export * from "./modules/authorization"; diff --git a/packages/stark-rbac/src/modules/authorization.ts b/packages/stark-rbac/src/modules/authorization.ts new file mode 100644 index 0000000000..003b99b57c --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization.ts @@ -0,0 +1,5 @@ +export * from "./authorization/actions"; +export * from "./authorization/directives"; +export * from "./authorization/entities"; +export * from "./authorization/services"; +export * from "./authorization/authorization.module"; diff --git a/packages/stark-rbac/src/modules/authorization/actions.ts b/packages/stark-rbac/src/modules/authorization/actions.ts new file mode 100644 index 0000000000..e96f07b0b8 --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/actions.ts @@ -0,0 +1 @@ +export * from "./actions/authorization.actions"; diff --git a/packages/stark-rbac/src/modules/authorization/actions/authorization.actions.ts b/packages/stark-rbac/src/modules/authorization/actions/authorization.actions.ts new file mode 100644 index 0000000000..81cf26051b --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/actions/authorization.actions.ts @@ -0,0 +1,46 @@ +import { Action } from "@ngrx/store"; + +/** + * Actions related to RBAC authorization + */ +export enum StarkRBACAuthorizationActionsTypes { + RBAC_USER_NAVIGATION_UNAUTHORIZED = "[StarkRBAC] User navigation unauthorized", + RBAC_USER_NAVIGATION_UNAUTHORIZED_REDIRECTED = "[StarkRBAC] User navigation unauthorized redirected" +} + +/** + * Action to be triggered when the user has navigated to a route that he is not authorized to. + */ +export class StarkUserNavigationUnauthorized implements Action { + /** + * The type of action + * @link StarkRBACAuthorizationActionsTypes + */ + public readonly type: StarkRBACAuthorizationActionsTypes.RBAC_USER_NAVIGATION_UNAUTHORIZED = + StarkRBACAuthorizationActionsTypes.RBAC_USER_NAVIGATION_UNAUTHORIZED; + + /** + * Class constructor + * @param targetState - The state the user is navigating to. + */ + public constructor(public targetState: string) {} +} + +/** + * Action to be triggered when the user is redirected because he is not authorized to navigate to the original route. + */ +export class StarkUserNavigationUnauthorizedRedirected implements Action { + /** + * The type of action + * @link StarkRBACAuthorizationActionsTypes + */ + public readonly type: StarkRBACAuthorizationActionsTypes.RBAC_USER_NAVIGATION_UNAUTHORIZED_REDIRECTED = + StarkRBACAuthorizationActionsTypes.RBAC_USER_NAVIGATION_UNAUTHORIZED_REDIRECTED; + + /** + * Class constructor + * @param targetState - The state the user is navigating to + * @param redirectionState - The redirection to be performed instead of the original navigation + */ + public constructor(public targetState: string, public redirectionState: string) {} +} diff --git a/packages/stark-rbac/src/modules/authorization/authorization.module.ts b/packages/stark-rbac/src/modules/authorization/authorization.module.ts new file mode 100644 index 0000000000..f343c51d43 --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/authorization.module.ts @@ -0,0 +1,58 @@ +import { ApplicationInitStatus, Inject, ModuleWithProviders, NgModule, Optional, Self, SkipSelf } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { from } from "rxjs"; +import { STARK_RBAC_AUTHORIZATION_SERVICE, StarkRBACAuthorizationService, StarkRBACAuthorizationServiceImpl } from "./services"; +import { StarkHideOnPermissionDirective, StarkShowOnPermissionDirective } from "./directives"; + +@NgModule({ + declarations: [StarkHideOnPermissionDirective, StarkShowOnPermissionDirective], + imports: [CommonModule], + exports: [StarkHideOnPermissionDirective, StarkShowOnPermissionDirective] +}) +export class StarkRBACAuthorizationModule { + /** + * Instantiates the services only once since they should be singletons + * so the forRoot() should be called only by the AppModule + * @link https://angular.io/guide/singleton-services#forroot + * @returns a module with providers + */ + public static forRoot(): ModuleWithProviders { + return { + ngModule: StarkRBACAuthorizationModule, + providers: [{ provide: STARK_RBAC_AUTHORIZATION_SERVICE, useClass: StarkRBACAuthorizationServiceImpl }] + }; + } + + /** + * Verifies that the 'forRoot' method of this module has been called so that its providers are effectively registered. + * @param rootAuthorizationService - The RBAC authorization service of the application (null when the 'forRoot' method was called) + * @param childAuthorizationService - The RBAC authorization service of the application (null when the 'forRoot' was not called, so this module is imported as a child module) + * @param appInitStatus - A class that reflects the state of running {@link APP_INITIALIZER}s + */ + public constructor( + @Optional() @SkipSelf() @Inject(STARK_RBAC_AUTHORIZATION_SERVICE) rootAuthorizationService: StarkRBACAuthorizationService, + @Optional() @Self() @Inject(STARK_RBAC_AUTHORIZATION_SERVICE) childAuthorizationService: StarkRBACAuthorizationService, + appInitStatus: ApplicationInitStatus + ) { + if (!childAuthorizationService && !rootAuthorizationService) { + throw new Error( + `${STARK_RBAC_AUTHORIZATION_SERVICE.toString()} is not provided. Make sure you call the 'forRoot' method of the StarkRBACAuthorizationModule in the AppModule only.` + ); + } + + if (childAuthorizationService && rootAuthorizationService) { + throw new Error( + `${STARK_RBAC_AUTHORIZATION_SERVICE.toString()} is already provided. Make sure you call the 'forRoot' method of the StarkRBACAuthorizationModule in the AppModule only.` + ); + } + + // initialize the service only when this is a Root module ('forRoot' was called) + if (childAuthorizationService && !rootAuthorizationService) { + // this logic cannot be executed in an APP_INITIALIZER factory because the StarkRBACAuthorizationService uses the StarkLoggingService + // which needs the "logging" state to be already defined in the Store (which NGRX defines internally via APP_INITIALIZER factories :p) + from(appInitStatus.donePromise).subscribe(() => { + childAuthorizationService.initializeService(); + }); + } + } +} diff --git a/packages/stark-rbac/src/modules/authorization/directives.ts b/packages/stark-rbac/src/modules/authorization/directives.ts new file mode 100644 index 0000000000..c46094130b --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/directives.ts @@ -0,0 +1,3 @@ +export * from "./directives/permission.intf"; +export * from "./directives/hide-on-permission.directive"; +export * from "./directives/show-on-permission.directive"; diff --git a/packages/stark-rbac/src/modules/authorization/directives/hide-on-permission.directive.spec.ts b/packages/stark-rbac/src/modules/authorization/directives/hide-on-permission.directive.spec.ts new file mode 100644 index 0000000000..16daf49667 --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/directives/hide-on-permission.directive.spec.ts @@ -0,0 +1,98 @@ +/* tslint:disable:completed-docs*/ +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { STARK_LOGGING_SERVICE } from "@nationalbankbelgium/stark-core"; +import { MockStarkLoggingService } from "@nationalbankbelgium/stark-core/testing"; +import { StarkRBACDirectivePermission } from "./permission.intf"; +import { StarkHideOnPermissionDirective } from "./hide-on-permission.directive"; +import { STARK_RBAC_AUTHORIZATION_SERVICE } from "../services"; +import { MockStarkRBACAuthorizationService } from "../testing/authorization.mock"; + +describe("StarkHideOnPermissionDirective", () => { + let fixture: ComponentFixture; + let hostComponent: TestComponent; + const hideOnPermission: StarkRBACDirectivePermission = { + roles: ["admin", "manager"] + }; + + @Component({ + selector: "test-component", + template: getTemplate("*starkHideOnPermission='hideOnPermission'") + }) + class TestComponent { + public hideOnPermission: StarkRBACDirectivePermission = hideOnPermission; + } + + function getTemplate(hideOnPermissionDirective: string): string { + return `
Protected content
`; + } + + function initializeComponentFixture(): void { + fixture = TestBed.createComponent(TestComponent); + hostComponent = fixture.componentInstance; + // trigger initial data binding + fixture.detectChanges(); + } + + const mockAuthorizationService: MockStarkRBACAuthorizationService = new MockStarkRBACAuthorizationService(); + + // Inject module dependencies + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StarkHideOnPermissionDirective, TestComponent], + imports: [], + providers: [ + { provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() }, + { provide: STARK_RBAC_AUTHORIZATION_SERVICE, useValue: mockAuthorizationService } + ] + }); + }); + + describe("on initialization", () => { + it("should throw an error in case there is no configuration object provided", () => { + const newTemplate: string = getTemplate("*starkHideOnPermission=''"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + TestBed.compileComponents().catch(() => fail("Component compilation failed")); + + expect(() => initializeComponentFixture()).toThrowError(/must contain 'roles'/); + }); + + /* tslint:disable-next-line:no-identical-functions */ + it("should throw an error in case the given configuration object has no 'roles' property defined", () => { + const newTemplate: string = getTemplate("*starkHideOnPermission='{}'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + TestBed.compileComponents().catch(() => fail("Component compilation failed")); + + expect(() => initializeComponentFixture()).toThrowError(/must contain 'roles'/); + }); + }); + + describe("authorization", () => { + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should NOT render the protected content ONLY in case the user HAS any of the specified roles", () => { + mockAuthorizationService.hasAnyRole.and.returnValue(true); + hostComponent.hideOnPermission = { ...hideOnPermission }; // set a new instance to trigger change detection in the directive + fixture.detectChanges(); + + let spanElement: NodeListOf = fixture.debugElement.nativeElement.querySelectorAll("span"); + expect(spanElement.length).toBe(0); + + mockAuthorizationService.hasAnyRole.and.returnValue(false); + hostComponent.hideOnPermission = { ...hideOnPermission }; // set a new instance to trigger change detection in the directive + fixture.detectChanges(); + + spanElement = fixture.debugElement.nativeElement.querySelectorAll("span"); + expect(spanElement.length).toBe(1); + expect(spanElement[0].textContent).toContain("Protected content"); + }); + }); +}); diff --git a/packages/stark-rbac/src/modules/authorization/directives/hide-on-permission.directive.ts b/packages/stark-rbac/src/modules/authorization/directives/hide-on-permission.directive.ts new file mode 100644 index 0000000000..586d7ef5ae --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/directives/hide-on-permission.directive.ts @@ -0,0 +1,87 @@ +import { Directive, Inject, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef, ViewRef } from "@angular/core"; +import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; +import { StarkRBACDirectivePermission } from "./permission.intf"; +import { STARK_RBAC_AUTHORIZATION_SERVICE, StarkRBACAuthorizationService } from "../services/authorization.service.intf"; + +/** + * Name of the directive + */ +const directiveName: string = "[starkHideOnPermission]"; + +/** + * [Structural Directive](https://angular.io/guide/structural-directives#structural-directives) to remove an element if the user has any of the roles specified + * in the {@link StarkRBACDirectivePermission} object passed as input. + * + * This directive should be used in cases where some content in the application should be shown to all the users except to those that have a specific role. + * + * @example + *
+ * + * Some protected content + *
+ */ +@Directive({ + selector: directiveName +}) +export class StarkHideOnPermissionDirective implements OnInit, OnDestroy { + /** + * {@link StarkRBACDirectivePermission} object containing any of the roles that the user must not have in order to display the element that this directive is applied to. + */ + @Input() + public set starkHideOnPermission(value: StarkRBACDirectivePermission) { + if (!value || typeof value.roles === "undefined") { + throw new Error(directiveName + ": Passed object must contain 'roles'."); + } + + if (!this.authorizationService.hasAnyRole(value.roles)) { + if (!this.viewRef) { + this.viewRef = this._viewContainer.createEmbeddedView(this._templateRef); + } else { + if (this._viewContainer.length === 0) { + this._viewContainer.insert(this.viewRef); + } + } + } else { + if (this._viewContainer.length === 1) { + this._viewContainer.detach(); + } + } + } + + /** + * @ignore + * @internal + * Reference to the view of the embedded template that will be inserted/removed based on the user permissions + */ + private viewRef?: ViewRef; + + /** + * Class constructor + * @param logger - The logger of the application + * @param authorizationService - The RBAC authorization service of the application + * @param _templateRef - The embedded template of the host element of this directive. + * @param _viewContainer - The container where one or more views can be attached to the host element of this directive. + */ + public constructor( + @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, + @Inject(STARK_RBAC_AUTHORIZATION_SERVICE) public authorizationService: StarkRBACAuthorizationService, + private _templateRef: TemplateRef, + private _viewContainer: ViewContainerRef + ) { + this.authorizationService = authorizationService; + } + + /** + * Component lifecycle hook + */ + public ngOnInit(): void { + this.logger.debug(directiveName + ": directive initialized"); + } + + /** + * Component lifecycle hook + */ + public ngOnDestroy(): void { + this._viewContainer.clear(); // destroy all views just in case + } +} diff --git a/packages/stark-rbac/src/modules/authorization/directives/permission.intf.ts b/packages/stark-rbac/src/modules/authorization/directives/permission.intf.ts new file mode 100644 index 0000000000..4f37489bfe --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/directives/permission.intf.ts @@ -0,0 +1,9 @@ +/** + * Describes the configuration to be passed to the {@link StarkShowOnPermissionDirective} and {@link StarkHideOnPermissionDirective}. + */ +export interface StarkRBACDirectivePermission { + /** + * The roles to be checked by the directives in order to determine whether the user has permission to access the content + */ + roles: string[]; +} diff --git a/packages/stark-rbac/src/modules/authorization/directives/show-on-permission.directive.spec.ts b/packages/stark-rbac/src/modules/authorization/directives/show-on-permission.directive.spec.ts new file mode 100644 index 0000000000..00121737ab --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/directives/show-on-permission.directive.spec.ts @@ -0,0 +1,98 @@ +/* tslint:disable:completed-docs*/ +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { STARK_LOGGING_SERVICE } from "@nationalbankbelgium/stark-core"; +import { MockStarkLoggingService } from "@nationalbankbelgium/stark-core/testing"; +import { StarkRBACDirectivePermission } from "./permission.intf"; +import { StarkShowOnPermissionDirective } from "./show-on-permission.directive"; +import { STARK_RBAC_AUTHORIZATION_SERVICE } from "../services"; +import { MockStarkRBACAuthorizationService } from "../testing/authorization.mock"; + +describe("StarkShowOnPermissionDirective", () => { + let fixture: ComponentFixture; + let hostComponent: TestComponent; + const showOnPermission: StarkRBACDirectivePermission = { + roles: ["admin", "manager"] + }; + + @Component({ + selector: "test-component", + template: getTemplate("*starkShowOnPermission='showOnPermission'") + }) + class TestComponent { + public showOnPermission: StarkRBACDirectivePermission = showOnPermission; + } + + function getTemplate(showOnPermissionDirective: string): string { + return `
Protected content
`; + } + + function initializeComponentFixture(): void { + fixture = TestBed.createComponent(TestComponent); + hostComponent = fixture.componentInstance; + // trigger initial data binding + fixture.detectChanges(); + } + + const mockAuthorizationService: MockStarkRBACAuthorizationService = new MockStarkRBACAuthorizationService(); + + // Inject module dependencies + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StarkShowOnPermissionDirective, TestComponent], + imports: [], + providers: [ + { provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() }, + { provide: STARK_RBAC_AUTHORIZATION_SERVICE, useValue: mockAuthorizationService } + ] + }); + }); + + describe("on initialization", () => { + it("should throw an error in case there is no configuration object provided", () => { + const newTemplate: string = getTemplate("*starkShowOnPermission=''"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + TestBed.compileComponents().catch(() => fail("Component compilation failed")); + + expect(() => initializeComponentFixture()).toThrowError(/must contain 'roles'/); + }); + + /* tslint:disable-next-line:no-identical-functions */ + it("should throw an error in case the given configuration object has no 'roles' property defined", () => { + const newTemplate: string = getTemplate("*starkShowOnPermission='{}'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + TestBed.compileComponents().catch(() => fail("Component compilation failed")); + + expect(() => initializeComponentFixture()).toThrowError(/must contain 'roles'/); + }); + }); + + describe("authorization", () => { + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the protected content ONLY in case the user HAS any of the specified roles", () => { + mockAuthorizationService.hasAnyRole.and.returnValue(true); + hostComponent.showOnPermission = { ...showOnPermission }; // set a new instance to trigger change detection in the directive + fixture.detectChanges(); + + let spanElement: NodeListOf = fixture.debugElement.nativeElement.querySelectorAll("span"); + expect(spanElement.length).toBe(1); + expect(spanElement[0].textContent).toContain("Protected content"); + + mockAuthorizationService.hasAnyRole.and.returnValue(false); + hostComponent.showOnPermission = { ...showOnPermission }; // set a new instance to trigger change detection in the directive + fixture.detectChanges(); + + spanElement = fixture.debugElement.nativeElement.querySelectorAll("span"); + expect(spanElement.length).toBe(0); + }); + }); +}); diff --git a/packages/stark-rbac/src/modules/authorization/directives/show-on-permission.directive.ts b/packages/stark-rbac/src/modules/authorization/directives/show-on-permission.directive.ts new file mode 100644 index 0000000000..4dd19a73a8 --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/directives/show-on-permission.directive.ts @@ -0,0 +1,87 @@ +import { Directive, Inject, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef, ViewRef } from "@angular/core"; +import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; +import { StarkRBACDirectivePermission } from "./permission.intf"; +import { STARK_RBAC_AUTHORIZATION_SERVICE, StarkRBACAuthorizationService } from "../services/authorization.service.intf"; + +/** + * Name of the directive + */ +const directiveName: string = "[starkShowOnPermission]"; + +/** + * [Structural Directive](https://angular.io/guide/structural-directives#structural-directives) to show an element if the user has any of the roles specified + * in the {@link StarkRBACDirectivePermission} object passed as input. + * + * This directive should be used in cases where some content in the application should be shown only to those users that have a specific role. + * + * @example + *
+ * + * Some protected content + *
+ */ +@Directive({ + selector: directiveName +}) +export class StarkShowOnPermissionDirective implements OnInit, OnDestroy { + /** + * {@link StarkRBACDirectivePermission} object containing any of the roles that the user must have in order to display the element that this directive is applied to. + */ + @Input() + public set starkShowOnPermission(value: StarkRBACDirectivePermission) { + if (!value || typeof value.roles === "undefined") { + throw new Error(directiveName + ": Passed object must contain 'roles'."); + } + + if (this.authorizationService.hasAnyRole(value.roles)) { + if (!this.viewRef) { + this.viewRef = this._viewContainer.createEmbeddedView(this._templateRef); + } else { + if (this._viewContainer.length === 0) { + this._viewContainer.insert(this.viewRef); + } + } + } else { + if (this._viewContainer.length === 1) { + this._viewContainer.detach(); + } + } + } + + /** + * @ignore + * @internal + * Reference to the view of the embedded template that will be inserted/removed based on the user permissions + */ + private viewRef?: ViewRef; + + /** + * Class constructor + * @param logger - The logger of the application + * @param authorizationService - The RBAC authorization service of the application + * @param _templateRef - The embedded template of the host element of this directive. + * @param _viewContainer - The container where one or more views can be attached to the host element of this directive. + */ + public constructor( + @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, + @Inject(STARK_RBAC_AUTHORIZATION_SERVICE) public authorizationService: StarkRBACAuthorizationService, + private _templateRef: TemplateRef, + private _viewContainer: ViewContainerRef + ) { + this.authorizationService = authorizationService; + } + + /** + * Component lifecycle hook + */ + public ngOnInit(): void { + this.logger.debug(directiveName + ": directive initialized"); + } + + /** + * Component lifecycle hook + */ + public ngOnDestroy(): void { + this._viewContainer.clear(); // destroy all views just in case + } +} diff --git a/packages/stark-rbac/src/modules/authorization/entities.ts b/packages/stark-rbac/src/modules/authorization/entities.ts new file mode 100644 index 0000000000..362347317c --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/entities.ts @@ -0,0 +1,2 @@ +export * from "./entities/state-permissions.entity.intf"; +export * from "./entities/state-redirection.intf"; diff --git a/packages/stark-rbac/src/modules/authorization/entities/state-permissions.entity.intf.ts b/packages/stark-rbac/src/modules/authorization/entities/state-permissions.entity.intf.ts new file mode 100644 index 0000000000..6f36ca7d62 --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/entities/state-permissions.entity.intf.ts @@ -0,0 +1,67 @@ +import { Ng2StateDeclaration } from "@uirouter/angular"; +import { StarkStateRedirection, StarkStateRedirectionFn } from "./state-redirection.intf"; + +/** + * Describes the content of the 'permissions' object to be set inside the 'data' property of a Router state configuration + */ +export interface StarkRBACStatePermissions { + /** + * Array of roles that the user must have in order to have permission to navigate to this route + */ + only?: string[]; + /** + * Array of roles that the user must not have in order to have permission to navigate to this route + */ + except?: string[]; + /** + * The redirection to be performed by the Router in case the user does not have permission to navigate to this route + */ + redirectTo?: StarkStateRedirection | StarkStateRedirectionFn; +} + +/** + * Describes a Router state configuration with 'data.permissions' defined in order to protect such state(s) with RBAC authorization + * via the {@link StarkRBACAuthorizationService}. + * + * **IMPORTANT:** Although the [Ng2StateDeclaration](https://ui-router.github.io/ng2/docs/latest/interfaces/state.ng2statedeclaration.html) can + * be used to define Router state configurations, it is recommended to use this interface instead because it clearly indicates the intention + * to protect the given state(s) and also enables the IDE to provide code completion and code hinting. + * + * ```typescript + * export const APP_STATES: StarkRBACStateDeclaration[] = [ + * { + * name: "someState", + * url: "/someUrl", + * component: SomeComponent, + * data: { + * ... + * permissions: { + * only: ["roleA", "roleB"] // or define 'except' option instead + * redirectTo: { + * stateName: "anotherState", + * params: {...} + * } + * } + * } + * }, + * ... + * ]; + * ``` + */ +export interface StarkRBACStateDeclaration extends Ng2StateDeclaration { + /** + * An inherited property to store state data + * + * Child states' `data` object will prototypally inherit from their parent state. + * + * This is the right spot to store RBAC authorization info (`permissions`) + * + * Note: because prototypal inheritance is used, changes to parent `data` objects reflect in the child `data` objects. + * Care should be taken if you are using `hasOwnProperty` on the `data` object. + * Properties from parent objects will return false for `hasOwnProperty`. + */ + data: { + permissions: StarkRBACStatePermissions; + [prop: string]: any; + }; +} diff --git a/packages/stark-rbac/src/modules/authorization/entities/state-redirection.intf.ts b/packages/stark-rbac/src/modules/authorization/entities/state-redirection.intf.ts new file mode 100644 index 0000000000..dc0289b6df --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/entities/state-redirection.intf.ts @@ -0,0 +1,21 @@ +import { Transition } from "@uirouter/core"; + +/** + * Defines a redirection to be performed by the Router + */ +export interface StarkStateRedirection { + /** + * The target state to redirect to. + */ + stateName: string; + /** + * The parameters to be passed to the redirection state. + */ + params?: object; +} + +/** + * Describes a function invoked passing the current {@link Transition} which should return the redirection to be performed by the Router. + * This is handy in case the redirection will happen only on specific cases. + */ +export type StarkStateRedirectionFn = (transition: Transition) => StarkStateRedirection; diff --git a/packages/stark-rbac/src/modules/authorization/services.ts b/packages/stark-rbac/src/modules/authorization/services.ts new file mode 100644 index 0000000000..2f3e266ec7 --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/services.ts @@ -0,0 +1,2 @@ +export { StarkRBACAuthorizationService, STARK_RBAC_AUTHORIZATION_SERVICE } from "./services/authorization.service.intf"; +export * from "./services/authorization.service"; diff --git a/packages/stark-rbac/src/modules/authorization/services/authorization.service.intf.ts b/packages/stark-rbac/src/modules/authorization/services/authorization.service.intf.ts new file mode 100644 index 0000000000..9f48d26e5f --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/services/authorization.service.intf.ts @@ -0,0 +1,40 @@ +import { InjectionToken } from "@angular/core"; + +/** + * The name of the service + */ +export const starkRBACAuthorizationServiceName: string = "StarkRBACAuthorizationService"; +/** + * Injection Token version of the Service Name + */ +export const STARK_RBAC_AUTHORIZATION_SERVICE: InjectionToken = new InjectionToken< + StarkRBACAuthorizationService +>(starkRBACAuthorizationServiceName); + +/** + * Service to be used in order to know whether the user has an specific role or set of roles and whether it is an anonymous user. + */ +export interface StarkRBACAuthorizationService { + /** + * Method to be called right after the app initializes in order to get the necessary info to determine the permissions of the current user. + * This method is called internally by the {@link StarkRBACAuthorizationModule}. + * + * **IMPORTANT:** In case you don't want to import the {@link StarkRBACAuthorizationModule} but you only want to define your own + * implementation of the StarkRBACAuthorizationService, make sure you call this method right after the app initializes. + */ + initializeService(): void; + /** + * Whether the current principal has the specified role. + */ + hasRole(roleCode: string): boolean; + + /** + * Whether the current principal has any of the supplied roles (given as an array of strings). + */ + hasAnyRole(roleCodes: string[]): boolean; + + /** + * Whether the current principal is an anonymous user. + */ + isAnonymous(): boolean; +} diff --git a/packages/stark-rbac/src/modules/authorization/services/authorization.service.spec.ts b/packages/stark-rbac/src/modules/authorization/services/authorization.service.spec.ts new file mode 100644 index 0000000000..b0083e9959 --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/services/authorization.service.spec.ts @@ -0,0 +1,541 @@ +/* tslint:disable:completed-docs no-big-function*/ +import { of, throwError } from "rxjs"; +import { Store } from "@ngrx/store"; +import { fakeAsync, tick } from "@angular/core/testing"; +import { HookMatchCriteria, Predicate, RawParams, StateDeclaration, StateOrName, TargetState, Transition } from "@uirouter/core"; +import { + StarkLoggingService, + StarkRoutingService, + StarkRoutingTransitionHook, + StarkSessionService, + StarkUser +} from "@nationalbankbelgium/stark-core"; +import { MockStarkLoggingService, MockStarkRoutingService, MockStarkSessionService } from "@nationalbankbelgium/stark-core/testing"; + +import { StarkRBACStatePermissions, StarkStateRedirection, StarkStateRedirectionFn } from "../entities"; +import { StarkRBACAuthorizationServiceImpl, starkUnauthorizedUserError } from "./authorization.service"; +import { StarkUserNavigationUnauthorized, StarkUserNavigationUnauthorizedRedirected } from "../actions/authorization.actions"; +import createSpyObj = jasmine.createSpyObj; +import createSpy = jasmine.createSpy; +import Spy = jasmine.Spy; + +describe("StarkRBACAuthorizationService", () => { + let mockStore: Store; + let mockLogger: MockStarkLoggingService; + let mockSessionService: MockStarkSessionService; + let mockRoutingService: MockStarkRoutingService; + let authorizationService: AuthorizationServiceHelper; + const dummyRole: string = "super user"; + const dummyUnauthorizedStateName: string = "unauthorized state"; + + function getMockTransitionTargetStateWithPermissions(mockPermissions: StarkRBACStatePermissions): StateDeclaration { + return { + data: { + permissions: mockPermissions + } + }; + } + + beforeEach(() => { + mockStore = createSpyObj("store", ["dispatch", "select"]); + mockLogger = new MockStarkLoggingService(); + mockSessionService = new MockStarkSessionService(); + mockRoutingService = new MockStarkRoutingService(); + authorizationService = new AuthorizationServiceHelper(mockLogger, mockSessionService, mockRoutingService, mockStore); + }); + + describe("initializeService", () => { + it("should subscribe to the getCurrentUser$ observable to get the current user from the Stark User service", () => { + const mockUser: Partial = { roles: [] }; + mockSessionService.getCurrentUser.and.returnValue(of(mockUser)); + expect(authorizationService.user).toBeUndefined(); + + authorizationService.initializeService(); + + expect(authorizationService.user).toBe(mockUser); + }); + + it("should throw an error if the getCurrentUser$ observable emits an error", fakeAsync(() => { + mockSessionService.getCurrentUser.and.returnValue(throwError("dummy error")); + expect(authorizationService.user).toBeUndefined(); + + expect(() => { + authorizationService.initializeService(); + tick(); // to force async error to be thrown + }).toThrowError(/error while getting the user profile/); + + expect(authorizationService.user).toBeUndefined(); + })); + + it("should call registerTransitionHook function", () => { + const mockUser: Partial = { roles: [] }; + mockSessionService.getCurrentUser.and.returnValue(of(mockUser)); + spyOn(authorizationService, "registerTransitionHook"); + + authorizationService.initializeService(); + + expect(authorizationService.registerTransitionHook).toHaveBeenCalledTimes(1); + }); + }); + + describe("registerTransitionHook", () => { + it("should add transitionHook (onStart) matching all states with permissions except starkAppInit/starkAppExit children", () => { + authorizationService.registerTransitionHook(); + + expect(mockRoutingService.addTransitionHook).toHaveBeenCalledTimes(1); + expect(mockRoutingService.addTransitionHook.calls.argsFor(0)[0]).toBe(StarkRoutingTransitionHook.ON_START); + + const hookMatchCriteria: HookMatchCriteria = mockRoutingService.addTransitionHook.calls.argsFor(0)[1]; + + expect(hookMatchCriteria.entering).toBeDefined(); + + const matchingFn: Predicate = >hookMatchCriteria.entering; + const nonMatchingStates: object[] = [ + { name: "starkAppInit.state1" }, + { name: "starkAppInit.state2", data: {} }, + { name: "starkAppInit.stateX", data: { whatever: {} } }, + { name: "starkAppExit.state1" }, + { name: "starkAppExit.state2" }, + { name: "starkAppExit.stateX" }, + { abstract: true, name: "" } // root state + ]; + const matchingStates: object[] = [ + { name: "whatever.state1", data: { permissions: { only: [] } } }, + { name: "other.state2", data: { permissions: { except: [] } } }, + { name: "stateX", data: { permissions: {} } } + ]; + + for (const state of matchingStates) { + expect(matchingFn(state)).toBe(true); + } + + for (const state of nonMatchingStates) { + expect(matchingFn(state)).toBe(false); + } + + expect(mockRoutingService.addTransitionHook.calls.argsFor(0)[2]).toBeDefined(); + expect(mockRoutingService.addTransitionHook.calls.argsFor(0)[3]).toEqual({ priority: 900 }); + }); + + it("should resolve the promise when the onStart hook is triggered and the current user IS authorized", () => { + authorizationService.registerTransitionHook(); + + expect(mockRoutingService.addTransitionHook.calls.argsFor(0)[0]).toBe(StarkRoutingTransitionHook.ON_START); + const onStartHookCallback: Function = mockRoutingService.addTransitionHook.calls.argsFor(0)[2]; + + spyOn(authorizationService, "isNavigationAuthorized").and.returnValue(true); + spyOn(authorizationService, "handleUnauthorizedNavigation"); + + const mockPermissions: StarkRBACStatePermissions = { + only: [""] + }; + const mockTransition: Partial = { + to: () => getMockTransitionTargetStateWithPermissions(mockPermissions) + }; + + // trigger the onStart hook callback + const hookResult: boolean = onStartHookCallback(mockTransition); + expect(hookResult).toBe(true); + + expect(authorizationService.isNavigationAuthorized).toHaveBeenCalledTimes(1); + expect(authorizationService.isNavigationAuthorized).toHaveBeenCalledWith(mockPermissions); + expect(authorizationService.handleUnauthorizedNavigation).not.toHaveBeenCalled(); + }); + + it("should reject the promise with the value returned by handleUnauthorizedNavigation() when the user is NOT authorized", () => { + authorizationService.registerTransitionHook(); + + expect(mockRoutingService.addTransitionHook.calls.argsFor(0)[0]).toBe(StarkRoutingTransitionHook.ON_START); + const onStartHookCallback: Function = mockRoutingService.addTransitionHook.calls.argsFor(0)[2]; + + spyOn(authorizationService, "isNavigationAuthorized").and.returnValue(false); + spyOn(authorizationService, "handleUnauthorizedNavigation").and.returnValue("dummy rejection value"); + + const mockPermissions: StarkRBACStatePermissions = { + only: [""] + }; + const mockTransition: Partial = { + to: () => getMockTransitionTargetStateWithPermissions(mockPermissions) + }; + + // trigger the onStart hook callback + const hookResult: string = onStartHookCallback(mockTransition); + expect(hookResult).toBe("dummy rejection value"); + + expect(authorizationService.isNavigationAuthorized).toHaveBeenCalledTimes(1); + expect(authorizationService.isNavigationAuthorized).toHaveBeenCalledWith(mockPermissions); + expect(authorizationService.handleUnauthorizedNavigation).toHaveBeenCalledTimes(1); + expect(authorizationService.handleUnauthorizedNavigation).toHaveBeenCalledWith(mockPermissions, mockTransition); + }); + }); + + describe("hasRole", () => { + it("should return true only if the current user has the given role", () => { + const authorizedRole: string = dummyRole; + const unauthorizedRole: string = "simple mortal"; + const mockUserWithRoles: Partial = { + roles: [dummyRole, "manager"] + }; + authorizationService.user = mockUserWithRoles; + + expect(authorizationService.hasRole(authorizedRole)).toBe(true); + expect(authorizationService.hasRole(unauthorizedRole)).toBe(false); + }); + + it("should return false when the given role is empty or undefined", () => { + const mockUserWithRoles: Partial = { + roles: [dummyRole, "manager", "", " "] // empty string is not a valid role! + }; + authorizationService.user = mockUserWithRoles; + + expect(authorizationService.hasRole(" ")).toBe(false); + expect(authorizationService.hasRole(undefined)).toBe(false); + }); + + it("should return false when there is no current user defined or it has no roles", () => { + const someRole: string = "whatever"; + authorizationService.user = undefined; + + expect(authorizationService.hasRole(someRole)).toBe(false); + + const mockUserWithEmptyRoles: Partial = { roles: [] }; + + authorizationService.user = mockUserWithEmptyRoles; + + expect(authorizationService.hasRole(someRole)).toBe(false); + }); + }); + + describe("hasAnyRole", () => { + it("should return true only if the current user has ANY of the given roles", () => { + const authorizedRoles: string[] = [dummyRole, "admin"]; + const unauthorizedRoles: string[] = ["simple mortal", "get out of here"]; + let mockUserWithRoles: Partial = { + roles: [dummyRole, "manager"] + }; + authorizationService.user = mockUserWithRoles; + + expect(authorizationService.hasAnyRole(authorizedRoles)).toBe(true); + expect(authorizationService.hasAnyRole(unauthorizedRoles)).toBe(false); + + mockUserWithRoles = { + roles: ["vip", "admin"] + }; + authorizationService.user = mockUserWithRoles; + + expect(authorizationService.hasAnyRole(authorizedRoles)).toBe(true); + expect(authorizationService.hasAnyRole(unauthorizedRoles)).toBe(false); + }); + + it("should return false when the given roles are empty or undefined", () => { + const mockUserWithRoles: Partial = { + roles: [dummyRole, "manager", "", " "] // empty string is not a valid role! + }; + authorizationService.user = mockUserWithRoles; + + expect(authorizationService.hasAnyRole([])).toBe(false); + expect(authorizationService.hasAnyRole(["", " "])).toBe(false); + expect(authorizationService.hasAnyRole(undefined)).toBe(false); + }); + + it("should return false when there is no current user defined or it has no roles", () => { + const someRoles: string[] = ["whatever"]; + authorizationService.user = undefined; + + expect(authorizationService.hasAnyRole(someRoles)).toBe(false); + + const mockUserWithEmptyRoles: Partial = { roles: [] }; + + authorizationService.user = mockUserWithEmptyRoles; + + expect(authorizationService.hasAnyRole(someRoles)).toBe(false); + }); + }); + + describe("isAnonymous", () => { + it("should return true only if the current user has isAnonymous property set to TRUE", () => { + let mockUserAnonymous: Partial = { isAnonymous: true }; + authorizationService.user = mockUserAnonymous; + + expect(authorizationService.isAnonymous()).toBe(true); + + mockUserAnonymous = { isAnonymous: false }; + authorizationService.user = mockUserAnonymous; + + expect(authorizationService.isAnonymous()).toBe(false); + + mockUserAnonymous = { isAnonymous: undefined }; + authorizationService.user = mockUserAnonymous; + + expect(authorizationService.isAnonymous()).toBe(false); + + mockUserAnonymous = {}; + authorizationService.user = mockUserAnonymous; + + expect(authorizationService.isAnonymous()).toBe(false); + }); + + it("should return false when there is no current user defined", () => { + authorizationService.user = undefined; + + expect(authorizationService.isAnonymous()).toBe(false); + }); + }); + + describe("isNavigationAuthorized", () => { + it("should call hasAnyRole() with the permissions 'only' if they are defined and return the value returned by that function", () => { + const mockPermissions: StarkRBACStatePermissions = { + only: ["dummyRole", "superRole"] + }; + spyOn(authorizationService, "hasAnyRole").and.returnValues(true, false); + + let result: boolean = authorizationService.isNavigationAuthorized(mockPermissions); + + expect(result).toBe(true); + expect(authorizationService.hasAnyRole).toHaveBeenCalledTimes(1); + expect(authorizationService.hasAnyRole).toHaveBeenCalledWith(mockPermissions.only); + + (authorizationService.hasAnyRole).calls.reset(); + result = authorizationService.isNavigationAuthorized(mockPermissions); + + expect(result).toBe(false); + expect(authorizationService.hasAnyRole).toHaveBeenCalledTimes(1); + expect(authorizationService.hasAnyRole).toHaveBeenCalledWith(mockPermissions.only); + }); + + it("should call hasAnyRole() with the permissions 'except' if they are defined and return the INVERTED value that is returned", () => { + const mockPermissions: StarkRBACStatePermissions = { + except: ["dummyRole", "superRole"] + }; + spyOn(authorizationService, "hasAnyRole").and.returnValues(true, false); + + let result: boolean = authorizationService.isNavigationAuthorized(mockPermissions); + + expect(result).toBe(false); // inverted value of what hasAnyRole returns => 'except' + expect(authorizationService.hasAnyRole).toHaveBeenCalledTimes(1); + expect(authorizationService.hasAnyRole).toHaveBeenCalledWith(mockPermissions.except); + + (authorizationService.hasAnyRole).calls.reset(); + result = authorizationService.isNavigationAuthorized(mockPermissions); + + expect(result).toBe(true); // inverted value of what hasAnyRole returns => 'except' + expect(authorizationService.hasAnyRole).toHaveBeenCalledTimes(1); + expect(authorizationService.hasAnyRole).toHaveBeenCalledWith(mockPermissions.except); + }); + + it("should give preference to permissions 'only' over 'except' when both are defined and call hasAnyRole() with those", () => { + const mockPermissions: StarkRBACStatePermissions = { + only: ["dummyRole", "superRole"], + except: ["don't care"] + }; + spyOn(authorizationService, "hasAnyRole").and.returnValues(true, false); + + let result: boolean = authorizationService.isNavigationAuthorized(mockPermissions); + + expect(result).toBe(true); + expect(authorizationService.hasAnyRole).toHaveBeenCalledTimes(1); + expect(authorizationService.hasAnyRole).toHaveBeenCalledWith(mockPermissions.only); + + (authorizationService.hasAnyRole).calls.reset(); + result = authorizationService.isNavigationAuthorized(mockPermissions); + + expect(result).toBe(false); + expect(authorizationService.hasAnyRole).toHaveBeenCalledTimes(1); + expect(authorizationService.hasAnyRole).toHaveBeenCalledWith(mockPermissions.only); + }); + + it("should return true without calling hasAnyRole() when the permissions object has invalid 'only' nor 'except' or is undefined", () => { + const undefinedStatePermissionsStr: string = "could not find 'only' or 'except'"; + + spyOn(authorizationService, "hasAnyRole"); + + const mockPermissionsArray: StarkRBACStatePermissions[] = [ + {}, + { only: "not an array" }, + { except: "not an array" }, + { redirectTo: { stateName: "", params: {} } }, + undefined + ]; + + for (const mockPermissions of mockPermissionsArray) { + mockLogger.warn.calls.reset(); + + const result: boolean = authorizationService.isNavigationAuthorized(mockPermissions); + + expect(result).toBe(true); + expect(authorizationService.hasAnyRole).not.toHaveBeenCalled(); + + if (typeof mockPermissions !== "undefined") { + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn.calls.argsFor(0)[0]).toContain(undefinedStatePermissionsStr); + } else { + // if the permissions object is undefined, no warning is logged + expect(mockLogger.warn).not.toHaveBeenCalled(); + } + } + }); + }); + + describe("handleUnauthorizedNavigation", () => { + it("should log a warning and return the value of redirectNavigation() in case permissions object has 'redirectTo' defined", () => { + const mockPermissions: StarkRBACStatePermissions = { + redirectTo: { + stateName: "dummy redirection state", + params: { someParam: "whatever" } + } + }; + const mockTransition: Partial = {}; + spyOn(authorizationService, "redirectNavigation").and.returnValue("dummy redirection return value"); + + const result: any = authorizationService.handleUnauthorizedNavigation(mockPermissions, mockTransition); + + expect(result).toBe("dummy redirection return value"); + expect(authorizationService.redirectNavigation).toHaveBeenCalledTimes(1); + expect(authorizationService.redirectNavigation).toHaveBeenCalledWith(mockPermissions.redirectTo, mockTransition); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith(starkUnauthorizedUserError); + expect(mockStore.dispatch).not.toHaveBeenCalled(); + }); + + it("should dispatch a FAILURE action and throw an error when permissions object is undefined ot it has no 'redirectTo' defined", () => { + let mockPermissions: StarkRBACStatePermissions = {}; + const targetStateObj: any = { + name: createSpy("spyNameFn").and.returnValue(dummyUnauthorizedStateName) + }; + const mockTransition: Partial = { + targetState: createSpy("spyTargetStateFn").and.returnValue(targetStateObj) + }; + spyOn(authorizationService, "redirectNavigation"); + + expect(() => { + authorizationService.handleUnauthorizedNavigation(mockPermissions, mockTransition); + }).toThrowError(starkUnauthorizedUserError); + + expect(authorizationService.redirectNavigation).not.toHaveBeenCalled(); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith(starkUnauthorizedUserError); + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + expect(mockStore.dispatch).toHaveBeenCalledWith(new StarkUserNavigationUnauthorized(dummyUnauthorizedStateName)); + + mockPermissions = undefined; + mockLogger.warn.calls.reset(); + + expect(() => { + authorizationService.handleUnauthorizedNavigation(mockPermissions, mockTransition); + }).toThrowError(starkUnauthorizedUserError); + + expect(authorizationService.redirectNavigation).not.toHaveBeenCalled(); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith(starkUnauthorizedUserError); + }); + }); + + describe("redirectNavigation", () => { + // mock target state + let targetStateObj: any; + let mockTransition: Partial; + const mockRedirectToObj: StarkStateRedirection = { + stateName: "dummy redirection state", + params: { someParam: "whatever" } + }; + + beforeEach(() => { + targetStateObj = { + redirectionStateName: undefined, // custom prop + redirectionStateParams: undefined, // custom prop + redirectionStateParamsReplaced: undefined, // custom prop + // name() function as defined in the Ui-Router API + name: createSpy("spyNameFn").and.returnValue(dummyUnauthorizedStateName), + // withState() function as defined in the Ui-Router API + withState: createSpy("spyWithStateFn").and.callFake( + (state: StateOrName): TargetState => { + targetStateObj.redirectionStateName = state; + return targetStateObj; + } + ), + // withParams() function as defined in the Ui-Router API + withParams: createSpy("spyWithParamsFn").and.callFake( + (params: RawParams, replace: boolean): TargetState => { + targetStateObj.redirectionStateParams = params; + targetStateObj.redirectionStateParamsReplaced = replace; + return targetStateObj; + } + ) + }; + + mockTransition = { + targetState: createSpy("spyTargetStateFn").and.returnValue(targetStateObj) + }; + }); + + it("should log a warning, dispatch REDIRECTED action and return a redirection state based on permissions 'redirectTo' object", () => { + const result: TargetState = authorizationService.redirectNavigation(mockRedirectToObj, mockTransition); + + expect(result).toBe(targetStateObj); + expect(result["redirectionStateName"]).toBe(mockRedirectToObj.stateName); + expect(result["redirectionStateParams"]).toBe(mockRedirectToObj.params); + expect(result["redirectionStateParamsReplaced"]).toBe(true); + expect(mockTransition.targetState).toHaveBeenCalledTimes(1); + expect(targetStateObj.withState).toHaveBeenCalledTimes(1); + expect(targetStateObj.withState).toHaveBeenCalledWith(mockRedirectToObj.stateName); + expect(targetStateObj.withParams).toHaveBeenCalledTimes(1); + expect(targetStateObj.withParams).toHaveBeenCalledWith(mockRedirectToObj.params, true); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn.calls.argsFor(0)[0]).toContain("redirecting"); + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new StarkUserNavigationUnauthorizedRedirected(dummyUnauthorizedStateName, mockRedirectToObj.stateName) + ); + }); + + it("should log a warning, dispatch REDIRECTED action and return a redirection state based on permissions 'redirectTo' function", () => { + const mockRedirectToFn: StarkStateRedirectionFn = createSpy("spyTargetStateFn").and.returnValue(mockRedirectToObj); + + const result: TargetState = authorizationService.redirectNavigation(mockRedirectToFn, mockTransition); + expect(result).toBe(targetStateObj); + expect(result["redirectionStateName"]).toBe(mockRedirectToObj.stateName); + expect(result["redirectionStateParams"]).toBe(mockRedirectToObj.params); + expect(result["redirectionStateParamsReplaced"]).toBe(true); + expect(mockRedirectToFn).toHaveBeenCalledTimes(1); + expect(mockRedirectToFn).toHaveBeenCalledWith(mockTransition); + expect(mockTransition.targetState).toHaveBeenCalledTimes(1); + expect(targetStateObj.withState).toHaveBeenCalledTimes(1); + expect(targetStateObj.withState).toHaveBeenCalledWith(mockRedirectToObj.stateName); + expect(targetStateObj.withParams).toHaveBeenCalledTimes(1); + expect(targetStateObj.withParams).toHaveBeenCalledWith(mockRedirectToObj.params, true); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn.calls.argsFor(0)[0]).toContain("redirecting"); + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new StarkUserNavigationUnauthorizedRedirected(dummyUnauthorizedStateName, mockRedirectToObj.stateName) + ); + }); + }); +}); + +class AuthorizationServiceHelper extends StarkRBACAuthorizationServiceImpl { + public constructor( + logger: StarkLoggingService, + sessionService: StarkSessionService, + routingService: StarkRoutingService, + store: Store + ) { + super(logger, sessionService, routingService, store); + } + + public registerTransitionHook(): void { + super.registerTransitionHook(); + } + + public isNavigationAuthorized(permissions: StarkRBACStatePermissions): boolean { + return super.isNavigationAuthorized(permissions); + } + + public handleUnauthorizedNavigation(permissions: StarkRBACStatePermissions, transition: Transition): TargetState { + return super.handleUnauthorizedNavigation(permissions, transition); + } + + public redirectNavigation(redirectTo: StarkStateRedirection | StarkStateRedirectionFn, transition: Transition): TargetState { + return super.redirectNavigation(redirectTo, transition); + } +} diff --git a/packages/stark-rbac/src/modules/authorization/services/authorization.service.ts b/packages/stark-rbac/src/modules/authorization/services/authorization.service.ts new file mode 100644 index 0000000000..fe76d28baa --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/services/authorization.service.ts @@ -0,0 +1,166 @@ +/* tslint:disable:completed-docs*/ +import { Inject, Injectable } from "@angular/core"; +import { StateDeclaration, StateObject, TargetState, Transition } from "@uirouter/core"; +import { Store } from "@ngrx/store"; +import { + STARK_LOGGING_SERVICE, + STARK_ROUTING_SERVICE, + STARK_SESSION_SERVICE, + starkAppExitStateName, + starkAppInitStateName, + StarkLoggingService, + StarkRoutingService, + StarkRoutingTransitionHook, + StarkSessionService, + StarkUser +} from "@nationalbankbelgium/stark-core"; +import { StarkRBACAuthorizationService, starkRBACAuthorizationServiceName } from "./authorization.service.intf"; +import { StarkRBACStatePermissions, StarkStateRedirection, StarkStateRedirectionFn } from "../entities"; +import { StarkUserNavigationUnauthorized, StarkUserNavigationUnauthorizedRedirected } from "../actions/authorization.actions"; + +/** + * @ignore + */ +export const starkUnauthorizedUserError: string = starkRBACAuthorizationServiceName + " => user not authorized"; + +/** + * @ignore + */ +@Injectable() +export class StarkRBACAuthorizationServiceImpl implements StarkRBACAuthorizationService { + public user?: StarkUser; + + public constructor( + @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, + @Inject(STARK_SESSION_SERVICE) public sessionService: StarkSessionService, + @Inject(STARK_ROUTING_SERVICE) public routingService: StarkRoutingService, + private store: Store + ) { + this.logger.debug(starkRBACAuthorizationServiceName + " loaded"); + } + + /** + * To be called only when the app initializes + */ + public initializeService(): void { + this.sessionService.getCurrentUser().subscribe( + (user: StarkUser | undefined) => { + if (user) { + this.user = user; + } + }, + () => { + throw new Error(starkRBACAuthorizationServiceName + ": error while getting the user profile"); + } + ); + + this.registerTransitionHook(); + } + + public hasRole(roleCode: string): boolean { + if (this.user && roleCode && roleCode.trim() !== "") { + return this.user.roles.indexOf(roleCode) > -1; + } else { + return false; + } + } + + public hasAnyRole(roleCodes: string[]): boolean { + let hasAnyRole: boolean = false; + + if (roleCodes instanceof Array) { + for (const roleCode of roleCodes) { + if (this.hasRole(roleCode)) { + hasAnyRole = true; + break; + } + } + } + + return hasAnyRole; + } + + public isAnonymous(): boolean { + if (this.user) { + return this.user.isAnonymous === true; + } else { + return false; + } + } + + protected registerTransitionHook(): void { + this.routingService.addKnownNavigationRejectionCause(starkUnauthorizedUserError); + + this.routingService.addTransitionHook( + StarkRoutingTransitionHook.ON_START, + { + // match only states with permissions except the ones that are children of starkAppInit/starkAppExit or the Ui-Router's root state + entering: (state?: StateObject) => { + const regexInitExitStateName: RegExp = new RegExp("(" + starkAppInitStateName + "|" + starkAppExitStateName + ")"); + if ( + state && + typeof state.name !== "undefined" && + !state.name.match(regexInitExitStateName) && + !(state.abstract && state.name === "") + ) { + return state.data && typeof state.data.permissions !== "undefined"; + } + return false; + } + }, + (transition: Transition): boolean | TargetState => { + const state: StateDeclaration = transition.to(); + const permissions: StarkRBACStatePermissions = state.data.permissions; + + if (!this.isNavigationAuthorized(permissions)) { + return this.handleUnauthorizedNavigation(permissions, transition); + } + + return true; + }, + { priority: 900 } // very high priority (this should be one of the first hooks to be called to reject/redirect transitions immediately) + ); + } + + protected isNavigationAuthorized(permissions: StarkRBACStatePermissions): boolean { + if (permissions) { + if (permissions.only instanceof Array) { + return this.hasAnyRole(permissions.only); + } else if (permissions.except instanceof Array) { + return !this.hasAnyRole(permissions.except); + } else { + this.logger.warn(starkRBACAuthorizationServiceName + ": could not find 'only' or 'except' in state 'data.permissions'"); + } + } + + return true; + } + + protected handleUnauthorizedNavigation(permissions: StarkRBACStatePermissions, transition: Transition): TargetState { + this.logger.warn(starkUnauthorizedUserError); + if (permissions && permissions.redirectTo) { + return this.redirectNavigation(permissions.redirectTo, transition); + } else { + // dispatch action so an effect can run any logic if needed + this.store.dispatch(new StarkUserNavigationUnauthorized(transition.targetState().name())); + throw new Error(starkUnauthorizedUserError); + } + } + + protected redirectNavigation(redirectTo: StarkStateRedirection | StarkStateRedirectionFn, transition: Transition): TargetState { + let stateRedirection: StarkStateRedirection; + + if (redirectTo instanceof Function) { + stateRedirection = redirectTo(transition); + } else { + stateRedirection = redirectTo; + } + + this.logger.warn(starkRBACAuthorizationServiceName + ": redirecting to state '" + stateRedirection.stateName + "'"); + const originalTargetState: TargetState = transition.targetState(); + // dispatch action so an effect can run any logic if needed + this.store.dispatch(new StarkUserNavigationUnauthorizedRedirected(originalTargetState.name(), stateRedirection.stateName)); + // overriding the target state with that one to be redirected to + return originalTargetState.withState(stateRedirection.stateName).withParams(stateRedirection.params || {}, true); + } +} diff --git a/packages/stark-rbac/src/modules/authorization/testing.ts b/packages/stark-rbac/src/modules/authorization/testing.ts new file mode 100644 index 0000000000..3d43ff0e07 --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/testing.ts @@ -0,0 +1 @@ +export * from "./testing/authorization.mock"; diff --git a/packages/stark-rbac/src/modules/authorization/testing/authorization.mock.ts b/packages/stark-rbac/src/modules/authorization/testing/authorization.mock.ts new file mode 100644 index 0000000000..0ebf4e5889 --- /dev/null +++ b/packages/stark-rbac/src/modules/authorization/testing/authorization.mock.ts @@ -0,0 +1,29 @@ +import { StarkRBACAuthorizationService } from "@nationalbankbelgium/stark-rbac"; +import SpyObj = jasmine.SpyObj; +import createSpy = jasmine.createSpy; + +/** + * Mock class of the StarkRBACAuthorizationService interface. + * @link StarkRBACAuthorizationService + */ +export class MockStarkRBACAuthorizationService implements SpyObj { + /** + * Method to be called right after the app initializes in order to get the necessary info to determine the permissions of the current user. + */ + public initializeService: SpyObj["initializeService"] = createSpy("initializeService"); + + /** + * Whether the current principal has the specified role. + */ + public hasRole: SpyObj["hasRole"] = createSpy("hasRole"); + + /** + * Whether the current principal has any of the supplied roles (given as an array of strings). + */ + public hasAnyRole: SpyObj["hasAnyRole"] = createSpy("hasAnyRole"); + + /** + * Whether the current principal is an anonymous user. + */ + public isAnonymous: SpyObj["isAnonymous"] = createSpy("isAnonymous"); +} diff --git a/packages/stark-rbac/src/stark-rbac.ts b/packages/stark-rbac/src/stark-rbac.ts new file mode 100644 index 0000000000..7554177cd2 --- /dev/null +++ b/packages/stark-rbac/src/stark-rbac.ts @@ -0,0 +1,2 @@ +// Export all submodules here +export * from "./modules"; diff --git a/packages/stark-rbac/testing/index.ts b/packages/stark-rbac/testing/index.ts new file mode 100644 index 0000000000..023feb5cf0 --- /dev/null +++ b/packages/stark-rbac/testing/index.ts @@ -0,0 +1,6 @@ +// This file is not used to build this module. It is only used during editing +// by the TypeScript language service and during build for verification. `ngc` +// replaces this file with production index.ts when it rewrites private symbol +// names. + +export * from "./public_api"; diff --git a/packages/stark-rbac/testing/package.json b/packages/stark-rbac/testing/package.json new file mode 100644 index 0000000000..b4e3acd903 --- /dev/null +++ b/packages/stark-rbac/testing/package.json @@ -0,0 +1,15 @@ +{ + "name": "@nationalbankbelgium/stark-rbac/testing", + "types": "testing.d.ts", + "main": "../bundles/stark-rbac-testing.umd.js", + "module": "../fesm5/testing.js", + "es2015": "../fesm2015/testing.js", + "esm5": "../esm5/testing/testing.js", + "esm2015": "../esm2015/testing/testing.js", + "fesm5": "../fesm5/testing.js", + "fesm2015": "../fesm2015/testing.js", + "scripts": { + "ngc": "node ../../../node_modules/@angular/compiler-cli/src/main.js -p ./tsconfig-build.json", + "tsc": "node ../../../node_modules/typescript/bin/tsc -p ./tsconfig-build.json" + } +} diff --git a/packages/stark-rbac/testing/public_api.ts b/packages/stark-rbac/testing/public_api.ts new file mode 100644 index 0000000000..cca3745f21 --- /dev/null +++ b/packages/stark-rbac/testing/public_api.ts @@ -0,0 +1,6 @@ +/** + * Entry point for all public APIs of this package. + */ +export * from "../src/modules/authorization/testing"; + +// This file only reexports content of the `src/modules/**/testing` folders. Keep it that way. diff --git a/packages/stark-rbac/testing/rollup.config.js b/packages/stark-rbac/testing/rollup.config.js new file mode 100644 index 0000000000..e66cda8df3 --- /dev/null +++ b/packages/stark-rbac/testing/rollup.config.js @@ -0,0 +1,22 @@ +"use strict"; + +const commonData = require("../../rollup.config.common-data.js"); // common configuration between environments + +module.exports = { + input: "../../../dist/packages-dist/stark-rbac/fesm5/testing.js", + external: commonData.external, + plugins: commonData.plugins, + output: [ + { + file: "../../../dist/packages-dist/stark-rbac/bundles/stark-rbac-testing.umd.js", + globals: commonData.output.globals, + format: commonData.output.format, + exports: commonData.output.exports, + name: "stark.rbac.testing", + sourcemap: commonData.output.sourcemap, + amd: { + id: "@nationalbankbelgium/stark-rbac/testing" + } + } + ] +}; diff --git a/packages/stark-rbac/testing/tsconfig-build.json b/packages/stark-rbac/testing/tsconfig-build.json new file mode 100644 index 0000000000..ec9ca32c0e --- /dev/null +++ b/packages/stark-rbac/testing/tsconfig-build.json @@ -0,0 +1,45 @@ +{ + "extends": "../tsconfig-build.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "../", + "typeRoots": [ + "../node_modules/@types", + "../node_modules/@nationalbankbelgium/stark-testing/node_modules/@types", + "../../stark-build/typings", + "../typings" + ], + "paths": { + "cerialize": ["../../stark-core/node_modules/cerialize"], + "@ng-idle/*": ["../../stark-core/node_modules/@ng-idle/*"], + "@ngrx/*": ["../../stark-core/node_modules/@ngrx/*"], + "@ngx-translate/*": ["../../stark-core/node_modules/@ngx-translate/*"], + "@uirouter/*": ["../../stark-core/node_modules/@uirouter/*"], + "environments/environment": ["../../../dist/packages/stark-core/src/common/environment"], + "moment": ["../../stark-core/node_modules/moment"], + "@nationalbankbelgium/stark-core": ["../../../dist/packages/stark-core"], + "@nationalbankbelgium/stark-rbac": ["../"] + }, + "outDir": "../../../dist/packages/stark-rbac" + }, + + "files": ["public_api.ts"], + + // Unfortunately, all those options have to be written in every tsconfig file + "angularCompilerOptions": { + "generateCodeForLibraries": true, + "skipMetadataEmit": false, + "strictMetadataEmit": false, + "strictInjectionParameters": true, + "fullTemplateTypeCheck": true, + "annotationsAs": "static fields", + "enableLegacyTemplate": false, + "preserveWhitespaces": false, + "allowEmptyCodegenFiles": false, + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "enableResourceInlining": true, + "flatModuleOutFile": "testing.js", + "flatModuleId": "@nationalbankbelgium/stark-rbac/testing" + } +} diff --git a/packages/stark-rbac/tsconfig-build.json b/packages/stark-rbac/tsconfig-build.json new file mode 100644 index 0000000000..0712456ee4 --- /dev/null +++ b/packages/stark-rbac/tsconfig-build.json @@ -0,0 +1,50 @@ +{ + "extends": "../stark-build/tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": ".", + "typeRoots": ["./node_modules/@types", "../stark-build/typings"], + "lib": ["dom", "dom.iterable", "es2017"], + "paths": { + "@angularclass/hmr": ["../stark-core/node_modules/@angularclass/hmr"], + "@angular/animations": ["../../node_modules/@angular/animations"], + "@angular/common": ["../../node_modules/@angular/common"], + "@angular/core": ["../../node_modules/@angular/core"], + "@angular/router": ["../../node_modules/@angular/router"], + "@ng-idle/*": ["../stark-core/node_modules/@ng-idle/*"], + "@ngrx/*": ["../stark-core/node_modules/@ngrx/*"], + "@ngx-translate/*": ["../stark-core/node_modules/@ngx-translate/*"], + "@uirouter/*": ["../stark-core/node_modules/@uirouter/*"], + "cerialize": ["../stark-core/node_modules/cerialize"], + "class-validator": ["../stark-core/node_modules/class-validator"], + "moment": ["../stark-core/node_modules/moment"], + "ibantools": ["../stark-core/node_modules/ibantools"], + "lodash": ["../stark-core/node_modules/lodash"], + "rxjs/*": ["../../node_modules/rxjs/*"], + "environments/environment": ["../../dist/packages/stark-core/src/common/environment"], + "@nationalbankbelgium/stark-core": ["../../dist/packages/stark-core"], + "@nationalbankbelgium/stark-rbac": ["."] + }, + "outDir": "../../dist/packages/stark-rbac" + }, + + "files": ["public_api.ts"], + + // Unfortunately, all those options have to be written in every tsconfig file + "angularCompilerOptions": { + "generateCodeForLibraries": true, + "skipMetadataEmit": false, + "strictMetadataEmit": false, + "strictInjectionParameters": true, + "fullTemplateTypeCheck": true, + "annotationsAs": "static fields", + "enableLegacyTemplate": false, + "preserveWhitespaces": false, + "allowEmptyCodegenFiles": false, + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "enableResourceInlining": true, + "flatModuleOutFile": "stark-rbac.js", + "flatModuleId": "@nationalbankbelgium/stark-rbac" + } +} diff --git a/packages/stark-rbac/tsconfig.spec.json b/packages/stark-rbac/tsconfig.spec.json new file mode 100644 index 0000000000..0d124f7e34 --- /dev/null +++ b/packages/stark-rbac/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig-build.json", + "compilerOptions": { + "module": "commonjs", + "paths": { + "@ngrx/*": ["../stark-core/node_modules/@ngrx/*"], + "@ngx-translate/*": ["../stark-core/node_modules/@ngx-translate/*"], + "@uirouter/*": ["../stark-core/node_modules/@uirouter/*"], + "moment": ["../stark-core/node_modules/moment"], + "lodash-es": ["../stark-core/node_modules/lodash-es"], + "lodash-es/*": ["../stark-core/node_modules/lodash-es/*"], + "@nationalbankbelgium/stark-core/testing": ["../../dist/packages/stark-core/testing"], + "@nationalbankbelgium/stark-core": ["../../dist/packages/stark-core"], + "@nationalbankbelgium/stark-rbac": ["."] + } + }, + "files": null, + "include": ["**/*.ts"] +} diff --git a/packages/stark-rbac/tslint.json b/packages/stark-rbac/tslint.json new file mode 100644 index 0000000000..2c79d4983c --- /dev/null +++ b/packages/stark-rbac/tslint.json @@ -0,0 +1,31 @@ +{ + "extends": ["tslint:latest", "tslint-sonarts", "codelyzer", "tslint-config-prettier", "../stark-build/config/tslint.json"], + "rules": { + "completed-docs": [ + true, + { + "enums": true, + "variables": { + "tags": { "existence": ["ignore", "link", "param", "returns"] } + }, + "functions": { + "tags": { "existence": ["ignore", "link", "param", "returns"] } + }, + "interfaces": { + "tags": { "existence": ["ignore", "link", "param", "returns"] }, + "visibilities": ["exported", "internal"] + }, + "classes": { + "tags": { "existence": ["ignore", "link", "param", "returns"] }, + "visibilities": ["internal"] + }, + "methods": { + "tags": { "existence": ["ignore", "link", "param", "returns"] } + } + } + ], + "jsdoc-format": [true, "check-multiline-start"], + "no-redundant-jsdoc": true, + "use-host-property-decorator": false + } +} diff --git a/packages/stark-ui/README.md b/packages/stark-ui/README.md index 4922391114..9fd7f94d99 100644 --- a/packages/stark-ui/README.md +++ b/packages/stark-ui/README.md @@ -10,6 +10,9 @@ Stark's UI module (aka stark-ui) provides the UI related features of the Stark framework. It includes the UI component kit of Stark as well as other services and utilities. +The Stark-UI module depends on some functionalities provided by the Stark-Core module such as services. However you can use this module without Stark-Core +as long as you provide the same functionalities/services yourself. + **[Getting Started](https://stark.nbb.be/api-docs/stark-ui/latest/additional-documentation/getting-started.html)** ## Developer Guide diff --git a/packages/stark-ui/public_api.ts b/packages/stark-ui/public_api.ts index 5ce966c7fd..e4f25b127f 100644 --- a/packages/stark-ui/public_api.ts +++ b/packages/stark-ui/public_api.ts @@ -1,6 +1,4 @@ /** - * @module - * @description * Entry point for all public APIs of this package. */ export * from "./src/stark-ui"; diff --git a/packages/tsconfig.json b/packages/tsconfig.json index c0b9aad33b..b8fab7c530 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -24,6 +24,7 @@ "./stark-build/node_modules/@types", "./stark-build/typings", "./stark-core/node_modules/@types", + "./stark-rbac/node_modules/@types", "./stark-testing/node_modules/@types", "./stark-ui/node_modules/@types", "../node_modules/@types" diff --git a/showcase/package-lock.json b/showcase/package-lock.json index 4e51530b86..4bf5c0ec82 100644 --- a/showcase/package-lock.json +++ b/showcase/package-lock.json @@ -1344,8 +1344,8 @@ } }, "@nationalbankbelgium/stark-build": { - "version": "file:../dist/packages-dist/stark-build/nationalbankbelgium-stark-build-10.0.0-beta.6-1ff90ffe.tgz", - "integrity": "sha512-VkVpJRpU4GDlp/x96CK4hiCtgdBwYOy4j56bF/fHsNkHKEEESFaQcsibR/CYCbgDK8OXFgOCt5mpXw07JmeZXg==", + "version": "file:../dist/packages-dist/stark-build/nationalbankbelgium-stark-build-10.0.0-beta.6-1b376c0c.tgz", + "integrity": "sha512-tsy9rxTF6913T5quADqPWJ6+cy24QPhL31yF3tm2Ra1BXSzSV3qr287vUIkrLZk1p8LdguKWp/Yg4IAQAINt+Q==", "dev": true, "requires": { "@angular-builders/custom-webpack": "^7.3.1", @@ -1374,8 +1374,8 @@ } }, "@nationalbankbelgium/stark-core": { - "version": "file:../dist/packages-dist/stark-core/nationalbankbelgium-stark-core-10.0.0-beta.6-1ff90ffe.tgz", - "integrity": "sha512-fWD0xQCriMPboQfWv64vBNu026GSuPBHUGVht5d5icPcJGUZJSUhHFbd2d2qMm7uJQzWERTdfnWDlWH4blIVqw==", + "version": "file:../dist/packages-dist/stark-core/nationalbankbelgium-stark-core-10.0.0-beta.6-1b376c0c.tgz", + "integrity": "sha512-sqVCVv9rd554TQYCIHy3cHyohkkU78tqZigMVeXfT+cll9LtaZn0XhrI0ctfdpR18n+Ym7pU22OYgnHOccrj2A==", "requires": { "@angularclass/hmr": "^2.1.3", "@ng-idle/core": "^6.0.0-beta.3", @@ -1397,9 +1397,16 @@ "uuid": "^3.3.2" } }, + "@nationalbankbelgium/stark-rbac": { + "version": "file:../dist/packages-dist/stark-rbac/nationalbankbelgium-stark-rbac-10.0.0-beta.6-1b376c0c.tgz", + "integrity": "sha512-BaaZc2zQEgP20L3+iKARV2/spZd8CdN13qTmjn7lcJebZ3FeVpM2MDuiDaHYm0uvxFNGA50tC5pRb7hxzbxCOg==", + "requires": { + "@types/lodash-es": "^4.17.1" + } + }, "@nationalbankbelgium/stark-testing": { - "version": "file:../dist/packages-dist/stark-testing/nationalbankbelgium-stark-testing-10.0.0-beta.6-1ff90ffe.tgz", - "integrity": "sha512-H8wNSVU6Q/v5cvVasfD7PRbDWw5CIKJwajG92sjGioRaHRVs6tDfvnWA6wNxkmnArE5awJP9HbqhFAzCa490NQ==", + "version": "file:../dist/packages-dist/stark-testing/nationalbankbelgium-stark-testing-10.0.0-beta.6-1b376c0c.tgz", + "integrity": "sha512-SgEojfvj0TWDYMs6MAPvEQ7sii9tYcy6eWvmS1qTO5D/XwNhyUYriM1T4Ny9foFptNV6MhxZuoS+6+IlPCTNYg==", "dev": true, "requires": { "@types/jasmine": "^3.3.12", @@ -1448,14 +1455,14 @@ } }, "@nationalbankbelgium/stark-ui": { - "version": "file:../dist/packages-dist/stark-ui/nationalbankbelgium-stark-ui-10.0.0-beta.6-1ff90ffe.tgz", - "integrity": "sha512-6hQ8nVrgQyPHQ8XqvbalZrT5Qi94EfDeRmB17G6pWIO4RorPW8kWVezslr3FiOnwyDvMLN+i5OsoBCQjz6Gbtg==", + "version": "file:../dist/packages-dist/stark-ui/nationalbankbelgium-stark-ui-10.0.0-beta.6-1b376c0c.tgz", + "integrity": "sha512-VbxcM6xXBFpiPNF+M4ynd1QiuWO0ZXEXg1R0Qcf05/+EISXW5/N7+OZsx3CmUXwJ3tqJV3szbTZwSra40SSGhg==", "requires": { "@angular/material-moment-adapter": "^7.0.0", "@mdi/angular-material": "^3.3.92", "@types/lodash-es": "^4.17.1", "@types/nouislider": "^9.0.4", - "@types/prismjs": "^1.16.0", + "@types/prismjs": "^1.9.0", "angular2-text-mask": "^9.0.0", "normalize.css": "^8.0.1", "nouislider": "^13.1.1", diff --git a/showcase/package.json b/showcase/package.json index a69cff1ea3..71fc40a920 100644 --- a/showcase/package.json +++ b/showcase/package.json @@ -123,8 +123,9 @@ "@angular/platform-browser-dynamic": "~7.2.2", "@angular/platform-server": "~7.2.2", "@angular/router": "~7.2.2", - "@nationalbankbelgium/stark-core": "file:../dist/packages-dist/stark-core/nationalbankbelgium-stark-core-10.0.0-beta.6-1ff90ffe.tgz", - "@nationalbankbelgium/stark-ui": "file:../dist/packages-dist/stark-ui/nationalbankbelgium-stark-ui-10.0.0-beta.6-1ff90ffe.tgz", + "@nationalbankbelgium/stark-core": "file:../dist/packages-dist/stark-core/nationalbankbelgium-stark-core-10.0.0-beta.6-1b376c0c.tgz", + "@nationalbankbelgium/stark-rbac": "file:../dist/packages-dist/stark-rbac/nationalbankbelgium-stark-rbac-10.0.0-beta.6-1b376c0c.tgz", + "@nationalbankbelgium/stark-ui": "file:../dist/packages-dist/stark-ui/nationalbankbelgium-stark-ui-10.0.0-beta.6-1b376c0c.tgz", "@uirouter/visualizer": "~7.0.0", "angular-in-memory-web-api": "~0.8.0", "basscss": "~8.0.10", @@ -140,8 +141,8 @@ }, "devDependencies": { "@compodoc/compodoc": "~1.1.7", - "@nationalbankbelgium/stark-build": "file:../dist/packages-dist/stark-build/nationalbankbelgium-stark-build-10.0.0-beta.6-1ff90ffe.tgz", - "@nationalbankbelgium/stark-testing": "file:../dist/packages-dist/stark-testing/nationalbankbelgium-stark-testing-10.0.0-beta.6-1ff90ffe.tgz", + "@nationalbankbelgium/stark-build": "file:../dist/packages-dist/stark-build/nationalbankbelgium-stark-build-10.0.0-beta.6-1b376c0c.tgz", + "@nationalbankbelgium/stark-testing": "file:../dist/packages-dist/stark-testing/nationalbankbelgium-stark-testing-10.0.0-beta.6-1b376c0c.tgz", "@types/core-js": "~2.5.0", "@types/hammerjs": "~2.0.36", "@types/node": "~8.10.37", diff --git a/showcase/src/app/app-menu.config.ts b/showcase/src/app/app-menu.config.ts index da326361da..0b40499262 100644 --- a/showcase/src/app/app-menu.config.ts +++ b/showcase/src/app/app-menu.config.ts @@ -201,7 +201,7 @@ export const APP_MENU_CONFIG: StarkMenuConfig = { isEnabled: true, entries: [ { - id: "menu-stark-ui-directives-mask", + id: "menu-stark-ui-input-mask", label: "SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TITLE", isVisible: true, isEnabled: true, @@ -222,7 +222,7 @@ export const APP_MENU_CONFIG: StarkMenuConfig = { targetState: "demo-ui.transform-input-directive" }, { - id: "menu-stark-ui-directives-progress-indicator", + id: "menu-stark-ui-progress-indicator", label: "SHOWCASE.DEMO.PROGRESS_INDICATOR.TITLE", isVisible: true, isEnabled: true, @@ -238,6 +238,41 @@ export const APP_MENU_CONFIG: StarkMenuConfig = { } ] }, + { + label: "Stark RBAC", + menuGroups: [ + { + id: "menu-stark-rbac-directives", + label: "Directives", + isVisible: true, + isEnabled: true, + entries: [ + { + id: "menu-stark-rbac-authorization", + label: "SHOWCASE.DEMO_RBAC.DIRECTIVES.AUTHORIZATION.TITLE", + isVisible: true, + isEnabled: true, + targetState: "demo-rbac.authorization-directives" + } + ] + }, + { + id: "menu-stark-rbac-services", + label: "Services", + isVisible: true, + isEnabled: true, + entries: [ + { + id: "menu-stark-rbac-authorization-service", + label: "SHOWCASE.DEMO_RBAC.SERVICES.AUTHORIZATION.TITLE", + isVisible: true, + isEnabled: true, + targetState: "demo-rbac.authorization-service" + } + ] + } + ] + }, { label: "Stark Core", menuGroups: [ diff --git a/showcase/src/app/app.component.html b/showcase/src/app/app.component.html index 3fda68b7aa..f1f5031bf8 100644 --- a/showcase/src/app/app.component.html +++ b/showcase/src/app/app.component.html @@ -104,6 +104,16 @@ + + + + + + + + +

SHOWCASE.DEMO_RBAC.SERVICES.AUTHORIZATION.PROTECTED_ROUTES

+

+ + + + + diff --git a/showcase/src/app/demo-rbac/pages/authorization-service/demo-authorization-service-page.component.scss b/showcase/src/app/demo-rbac/pages/authorization-service/demo-authorization-service-page.component.scss new file mode 100644 index 0000000000..8d153ad4ba --- /dev/null +++ b/showcase/src/app/demo-rbac/pages/authorization-service/demo-authorization-service-page.component.scss @@ -0,0 +1,8 @@ +.nav-button-row { + display: flex; + justify-content: center; + + .rbac-demo-button { + margin: 10px; + } +} diff --git a/showcase/src/app/demo-rbac/pages/authorization-service/demo-authorization-service-page.component.ts b/showcase/src/app/demo-rbac/pages/authorization-service/demo-authorization-service-page.component.ts new file mode 100644 index 0000000000..5041c38ed9 --- /dev/null +++ b/showcase/src/app/demo-rbac/pages/authorization-service/demo-authorization-service-page.component.ts @@ -0,0 +1,68 @@ +import { Component } from "@angular/core"; +import { ReferenceLink } from "../../../shared/components/reference-block"; + +@Component({ + selector: "demo-authorization-service", + templateUrl: "./demo-authorization-service-page.component.html", + styleUrls: ["./demo-authorization-service-page.component.scss"] +}) +export class DemoAuthorizationServicePageComponent { + public referenceList: ReferenceLink[] = [ + { + label: "Stark RBAC Authorization service", + url: "https://stark.nbb.be/api-docs/stark-rbac/latest/interfaces/StarkRBACAuthorizationService.html" + }, + { + label: "Stark RBAC State declaration", + url: "https://stark.nbb.be/api-docs/stark-rbac/latest/interfaces/StarkRBACStateDeclaration.html" + }, + { + label: "Stark RBAC State permissions", + url: "https://stark.nbb.be/api-docs/stark-rbac/latest/interfaces/StarkRBACStatePermissions.html" + }, + { + label: "Stark RBAC State redirection", + url: "https://stark.nbb.be/api-docs/stark-rbac/latest/interfaces/StarkStateRedirection.html" + } + ]; + + public stateDeclarations: string = ` + import { DemoProtectedPageComponent } from "./pages"; + import { StarkRBACStateDeclaration } from "@nationalbankbelgium/stark-rbac"; + + const stateDeclarations: StarkRBACStateDeclaration[] = [ + { + name: "demo-rbac.protected-page-admin", + url: "/protected-page-admin", + data: { + permissions: { + only: ["admin"] + } + }, + views: { "@": { component: DemoProtectedPageComponent } } + }, + { + name: "demo-rbac.protected-page-manager", + url: "/protected-page-manager", + data: { + permissions: { + only: ["manager"] + } + }, + views: { "@": { component: DemoProtectedPageComponent } } + }, + { + name: "demo-rbac.protected-page-super-admin", + url: "/protected-page-manager", + data: { + permissions: { + only: ["super-admin"], + redirectTo: { + stateName: "news" + } + } + }, + views: { "@": { component: DemoProtectedPageComponent } } + } + ];`; +} diff --git a/showcase/src/app/demo-rbac/pages/authorization-service/index.ts b/showcase/src/app/demo-rbac/pages/authorization-service/index.ts new file mode 100644 index 0000000000..ce850772c1 --- /dev/null +++ b/showcase/src/app/demo-rbac/pages/authorization-service/index.ts @@ -0,0 +1 @@ +export * from "./demo-authorization-service-page.component"; diff --git a/showcase/src/app/demo-rbac/pages/index.ts b/showcase/src/app/demo-rbac/pages/index.ts new file mode 100644 index 0000000000..fea82ac79f --- /dev/null +++ b/showcase/src/app/demo-rbac/pages/index.ts @@ -0,0 +1,3 @@ +export * from "./authorization-directives"; +export * from "./authorization-service"; +export * from "./protected-page"; diff --git a/showcase/src/app/demo-rbac/pages/protected-page/demo-protected-page.component.html b/showcase/src/app/demo-rbac/pages/protected-page/demo-protected-page.component.html new file mode 100644 index 0000000000..4b351f25ec --- /dev/null +++ b/showcase/src/app/demo-rbac/pages/protected-page/demo-protected-page.component.html @@ -0,0 +1,7 @@ +

SHOWCASE.DEMO_RBAC.SERVICES.RESTRICTED_PAGE.TITLE

+ +

SHOWCASE.DEMO_RBAC.SERVICES.RESTRICTED_PAGE.DESCRIPTION

+ + diff --git a/showcase/src/app/demo-rbac/pages/protected-page/demo-protected-page.component.ts b/showcase/src/app/demo-rbac/pages/protected-page/demo-protected-page.component.ts new file mode 100644 index 0000000000..647803a3d3 --- /dev/null +++ b/showcase/src/app/demo-rbac/pages/protected-page/demo-protected-page.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "demo-protected-page", + templateUrl: "./demo-protected-page.component.html" +}) +export class DemoProtectedPageComponent {} diff --git a/showcase/src/app/demo-rbac/pages/protected-page/index.ts b/showcase/src/app/demo-rbac/pages/protected-page/index.ts new file mode 100644 index 0000000000..9e9b2368ad --- /dev/null +++ b/showcase/src/app/demo-rbac/pages/protected-page/index.ts @@ -0,0 +1 @@ +export * from "./demo-protected-page.component"; diff --git a/showcase/src/app/demo-rbac/routes.ts b/showcase/src/app/demo-rbac/routes.ts new file mode 100644 index 0000000000..7754e42e29 --- /dev/null +++ b/showcase/src/app/demo-rbac/routes.ts @@ -0,0 +1,59 @@ +import { Ng2StateDeclaration } from "@uirouter/angular"; +import { StarkRBACStateDeclaration } from "@nationalbankbelgium/stark-rbac"; +import { DemoAuthorizationDirectivesPageComponent, DemoAuthorizationServicePageComponent, DemoProtectedPageComponent } from "./pages"; + +export const DEMO_STATES: (Ng2StateDeclaration | StarkRBACStateDeclaration)[] = [ + { name: "demo-rbac", url: "^/demo-rbac", abstract: true, parent: "app" }, + { + name: "demo-rbac.authorization-directives", + url: "/authorization-directives", + data: { + translationKey: "SHOWCASE.DEMO_RBAC.AUTHORIZATION_DIRECTIVES.TITLE" + }, + views: { "@": { component: DemoAuthorizationDirectivesPageComponent } } + }, + { + name: "demo-rbac.authorization-service", + url: "/authorization-service", + data: { + translationKey: "SHOWCASE.DEMO_RBAC.AUTHORIZATION_SERVICE.TITLE" + }, + views: { "@": { component: DemoAuthorizationServicePageComponent } } + }, + { + name: "demo-rbac.protected-page-admin", + url: "/protected-page-admin", + data: { + translationKey: "SHOWCASE.DEMO_RBAC.PROTECTED_PAGE.TITLE", + permissions: { + only: ["admin"] + } + }, + views: { "@": { component: DemoProtectedPageComponent } } + }, + { + name: "demo-rbac.protected-page-manager", + url: "/protected-page-manager", + data: { + translationKey: "SHOULD NEVER BE SHOWN", + permissions: { + only: ["manager"] + } + }, + views: { "@": { component: DemoProtectedPageComponent } } + }, + { + name: "demo-rbac.protected-page-super-admin", + url: "/protected-page-manager", + data: { + translationKey: "SHOULD NEVER BE SHOWN", + permissions: { + only: ["super-admin"], + redirectTo: { + stateName: "news" + } + } + }, + views: { "@": { component: DemoProtectedPageComponent } } + } +]; diff --git a/showcase/src/app/demo-rbac/services/demo-authorization.service.ts b/showcase/src/app/demo-rbac/services/demo-authorization.service.ts new file mode 100644 index 0000000000..aa81854389 --- /dev/null +++ b/showcase/src/app/demo-rbac/services/demo-authorization.service.ts @@ -0,0 +1,24 @@ +import { StarkRBACAuthorizationServiceImpl } from "@nationalbankbelgium/stark-rbac"; +import { StarkUser } from "@nationalbankbelgium/stark-core"; +import { Injectable } from "@angular/core"; + +@Injectable() +export class DemoAuthorizationService extends StarkRBACAuthorizationServiceImpl { + /** + * To be called only when the app initializes + */ + public initializeService(): void { + this.sessionService.getCurrentUser().subscribe( + (user: StarkUser | undefined) => { + if (user) { + this.user = { ...user, roles: [...user.roles, "blabla"] }; + } + }, + () => { + throw new Error("DemoAuthorizationService: error while getting the user profile"); + } + ); + + this.registerTransitionHook(); + } +} diff --git a/showcase/src/app/shared/effects/index.ts b/showcase/src/app/shared/effects/index.ts index 09bd63e411..51b5d09465 100644 --- a/showcase/src/app/shared/effects/index.ts +++ b/showcase/src/app/shared/effects/index.ts @@ -1 +1,2 @@ export * from "./stark-error-handling.effects"; +export * from "./stark-rbac-unauthorized-navigation.effects"; diff --git a/showcase/src/app/shared/effects/stark-rbac-unauthorized-navigation.effects.ts b/showcase/src/app/shared/effects/stark-rbac-unauthorized-navigation.effects.ts new file mode 100644 index 0000000000..d28d9cd793 --- /dev/null +++ b/showcase/src/app/shared/effects/stark-rbac-unauthorized-navigation.effects.ts @@ -0,0 +1,82 @@ +import { Injectable, Injector, NgZone } from "@angular/core"; +import { Actions, Effect, ofType } from "@ngrx/effects"; +import { Observable } from "rxjs"; +import { map } from "rxjs/operators"; +import { + StarkRBACAuthorizationActionsTypes, + StarkUserNavigationUnauthorized, + StarkUserNavigationUnauthorizedRedirected +} from "@nationalbankbelgium/stark-rbac"; +import { STARK_TOAST_NOTIFICATION_SERVICE, StarkMessageType, StarkToastNotificationService } from "@nationalbankbelgium/stark-ui"; +import uniqueId from "lodash-es/uniqueId"; + +/** + * This class is used to determine what to do with an error + */ +@Injectable() +export class StarkRbacUnauthorizedNavigationEffects { + private _starkToastNotificationService: StarkToastNotificationService; + + /** + * Class constructor + * @param actions$ - the action to perform + * @param injector - the injector of the class + * @param zone - the service to execute actions inside or outside of an Angular Zone. + */ + public constructor(private actions$: Actions, private injector: Injector, private zone: NgZone) {} + + @Effect({ dispatch: false }) + public starkRBACNavigationUnauthorized$(): Observable { + return this.actions$.pipe( + ofType(StarkRBACAuthorizationActionsTypes.RBAC_USER_NAVIGATION_UNAUTHORIZED), + map((action: StarkUserNavigationUnauthorized) => { + this.zone.run(() => { + this.toastNotificationService + .show({ + id: uniqueId(), + type: StarkMessageType.ERROR, + key: action.type, + code: "Stark-RBAC: unauthorized navigation" + }) + .subscribe(); + }); + }) + ); + } + + @Effect({ dispatch: false }) + public starkRBACNavigationUnauthorizedRedirected$(): Observable { + return this.actions$.pipe( + ofType( + StarkRBACAuthorizationActionsTypes.RBAC_USER_NAVIGATION_UNAUTHORIZED_REDIRECTED + ), + map((action: StarkUserNavigationUnauthorizedRedirected) => { + this.zone.run(() => { + this.toastNotificationService + .show({ + id: uniqueId(), + type: StarkMessageType.WARNING, + key: "SHOWCASE.DEMO_RBAC.SERVICES.AUTHORIZATION.REDIRECTION_MESSAGE", + interpolateValues: { rbacActionType: action.type }, + code: "Stark-RBAC: unauthorized navigation redirected" + }) + .subscribe(); + }); + }) + ); + } + + // + + /** + * Gets the StarkToastNotificationService from the Injector. + * @throws When the service is not found (the ToastNotification module is not imported in the app) + */ + private get toastNotificationService(): StarkToastNotificationService { + if (typeof this._starkToastNotificationService === "undefined") { + this._starkToastNotificationService = this.injector.get(STARK_TOAST_NOTIFICATION_SERVICE); + return this._starkToastNotificationService; + } + return this._starkToastNotificationService; + } +} diff --git a/showcase/src/app/welcome/pages/getting-started/getting-started-page.component.html b/showcase/src/app/welcome/pages/getting-started/getting-started-page.component.html index 0f9ed2451d..3cb744205f 100644 --- a/showcase/src/app/welcome/pages/getting-started/getting-started-page.component.html +++ b/showcase/src/app/welcome/pages/getting-started/getting-started-page.component.html @@ -39,6 +39,11 @@

Packages installation

stark-ui +
  • + stark-rbac +
  • stark-build
    -

    -
    -
    +
    +

    +
    - -

    {{ "SHOWCASE.HOMEPAGE.CORE" | translate }}

    -
    +
    +

    {{ "SHOWCASE.HOMEPAGE.CORE" | translate }}

    +
    @@ -49,21 +53,54 @@
    -

    {{ "SHOWCASE.HOMEPAGE.UI" | translate }}

    -
    +
    +

    {{ "SHOWCASE.HOMEPAGE.UI" | translate }}

    +
    +
    +
    + +
    +
    +
    +
    +

    {{ "SHOWCASE.HOMEPAGE.RBAC" | translate }}

    +
    +
    @@ -71,17 +108,19 @@
    -

    {{ "SHOWCASE.HOMEPAGE.SHOWCASE" | translate }}

    -
    -
    -
    +
    +

    {{ "SHOWCASE.HOMEPAGE.SHOWCASE" | translate }}

    +
    +
    diff --git a/showcase/src/app/welcome/pages/news/_news-page-theme.scss b/showcase/src/app/welcome/pages/news/_news-page-theme.scss new file mode 100644 index 0000000000..b36b95eedf --- /dev/null +++ b/showcase/src/app/welcome/pages/news/_news-page-theme.scss @@ -0,0 +1,4 @@ +.custom-news-page-button { + color: mat-contrast($primary-palette, 500); + background-color: mat-color($primary-palette, 700); +} diff --git a/showcase/src/app/welcome/pages/news/_news-page.component.scss b/showcase/src/app/welcome/pages/news/_news-page.component.scss index 694128473f..da094d9f1d 100644 --- a/showcase/src/app/welcome/pages/news/_news-page.component.scss +++ b/showcase/src/app/welcome/pages/news/_news-page.component.scss @@ -22,7 +22,7 @@ align-items: baseline; } -.custom-homepage-button { +.custom-news-page-button { align-items: center; justify-content: center; width: 150px; @@ -31,10 +31,8 @@ font-size: 13px; font-weight: 600; line-height: 30px; - color: #fff; - background-color: #0063bb; border-radius: 48px; - box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .26); + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); box-sizing: border-box; cursor: pointer; display: block; @@ -45,67 +43,66 @@ .top-news news-item { .section-border { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - box-sizing: border-box; - margin: 16px auto; - width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + box-sizing: border-box; + margin: 16px auto; + width: 100%; } .news-item-container { - display: flex; - align-items: center; - width: 100%; - flex-direction: column; + display: flex; + align-items: center; + width: 100%; + flex-direction: column; } .news-item-title { - width: 100%; - font-size: 25px; + width: 100%; + font-size: 25px; } .news-item-content { - width: 100%; - font-size: 15px; + width: 100%; + font-size: 15px; } .information { - width: 100%; - font-size: 13px; + width: 100%; + font-size: 13px; } .news-image { - min-width: 350px; - height: 200px; - margin-right: 25px; + min-width: 350px; + height: 200px; + margin-right: 25px; } } -@media #{$handset-toc-query-screen}{ +@media #{$handset-toc-query-screen} { .news-container { - justify-content: center; - align-items: center; - flex-direction: column; - display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + display: flex; } .top-news news-item { - .section-border { - flex-direction: column; - width: 290px; - padding: 0 0 0 0; - } - .news-item-title { - font-size: 15px; - } - .news-item-content { - font-size: 13px; - } - .information { - font-size: 12px; - } - .news-image { - min-width: 290px; - height: 150px; - margin: 0 0 0 0; - } + .section-border { + flex-direction: column; + width: 290px; + padding: 0 0 0 0; + } + .news-item-title { + font-size: 15px; + } + .news-item-content { + font-size: 13px; + } + .information { + font-size: 12px; + } + .news-image { + min-width: 290px; + height: 150px; + margin: 0 0 0 0; + } } - } diff --git a/showcase/src/app/welcome/pages/news/news-page.component.html b/showcase/src/app/welcome/pages/news/news-page.component.html index aa75f42fc8..69872f7860 100644 --- a/showcase/src/app/welcome/pages/news/news-page.component.html +++ b/showcase/src/app/welcome/pages/news/news-page.component.html @@ -2,17 +2,20 @@
    -
    -
    +

    10.0.0-beta.3 Released!

    -

    In this release has been implemented, among others, a very efficient generic search component. - New documentation has also been added and some bugs have been fixed. -

    - +

    + In this release has been implemented, among others, a very efficient generic search component. New documentation has also + been added and some bugs have been fixed. +

    +
    Release Notes
    @@ -24,14 +27,16 @@

    10.0.0-beta.3 Released!

    10.0.0-beta.2 Released!

    -

    Big changes for Stark: it has been upgraded to Angular 7! New features such as a preloading - screen, - a session timeout warning, new stylings applicable to Stark table component have been added. +

    + Big changes for Stark: it has been upgraded to Angular 7! New features such as a preloading screen, a session timeout + warning, new stylings applicable to Stark table component have been added.

    - + Release Notes
    @@ -42,13 +47,15 @@

    10.0.0-beta.2 Released!

    10.0.0-beta.1 Released!

    - The next release of Stark is available right now! - This release provides lots of packages update, new components such as Message Pane or Minimap, the implementation of a custom error handler, ... + The next release of Stark is available right now! This release provides lots of packages update, new components such as + Message Pane or Minimap, the implementation of a custom error handler, ...

    - + Release Notes
    @@ -60,12 +67,9 @@

    A very happy occasion

    Stark Logo was born this week!

    - Let's all make a round of applause for our java colleague, Maxime, who had the good idea to mix - Angular's pretty logo with the name of our project! - Thanks also to Carlo, who rearranged it to make it more portable on other browsers such as IE! + Let's all make a round of applause for our java colleague, Maxime, who had the good idea to mix Angular's pretty logo with + the name of our project! Thanks also to Carlo, who rearranged it to make it more portable on other browsers such as IE!

    -
    - diff --git a/showcase/src/assets/examples/rbac-authorization-directives/hide-on-permission.html b/showcase/src/assets/examples/rbac-authorization-directives/hide-on-permission.html new file mode 100644 index 0000000000..a7581873f9 --- /dev/null +++ b/showcase/src/assets/examples/rbac-authorization-directives/hide-on-permission.html @@ -0,0 +1,12 @@ +
    + + + Admin + Manager + +
    + +
    + This content is protected.
    + It is only visible to an authorized user that doesn't have the 'manager' role. +
    diff --git a/showcase/src/assets/examples/rbac-authorization-directives/hide-on-permission.ts b/showcase/src/assets/examples/rbac-authorization-directives/hide-on-permission.ts new file mode 100644 index 0000000000..8660a60313 --- /dev/null +++ b/showcase/src/assets/examples/rbac-authorization-directives/hide-on-permission.ts @@ -0,0 +1,22 @@ +import { Component } from "@angular/core"; +import { MatButtonToggleChange } from "@angular/material/button-toggle"; +import { StarkRBACDirectivePermission } from "@nationalbankbelgium/stark-rbac"; + +@Component({ + selector: "demo-authorization-directives", + templateUrl: "./demo-authorization-directives-page.component.html", + styleUrls: ["./demo-authorization-directives-page.component.scss"] +}) +export class DemoAuthorizationDirectivesPageComponent { + public selectedUnauthorizedRole: string = "manager"; // current user's role + + public get permissionsConfig(): StarkRBACDirectivePermission { + return { + roles: [this.selectedUnauthorizedRole] + }; + } + + public onRoleChange(changeEvent: MatButtonToggleChange): void { + this.selectedUnauthorizedRole = changeEvent.value; + } +} diff --git a/showcase/src/assets/examples/rbac-authorization-directives/show-on-permission.html b/showcase/src/assets/examples/rbac-authorization-directives/show-on-permission.html new file mode 100644 index 0000000000..0030f3061a --- /dev/null +++ b/showcase/src/assets/examples/rbac-authorization-directives/show-on-permission.html @@ -0,0 +1,12 @@ +
    + + + Admin + Manager + +
    + +
    + This content is protected.
    + It is only visible to an authorized user that has the 'manager' role. +
    diff --git a/showcase/src/assets/examples/rbac-authorization-directives/show-on-permission.ts b/showcase/src/assets/examples/rbac-authorization-directives/show-on-permission.ts new file mode 100644 index 0000000000..d05b82c94a --- /dev/null +++ b/showcase/src/assets/examples/rbac-authorization-directives/show-on-permission.ts @@ -0,0 +1,22 @@ +import { Component } from "@angular/core"; +import { MatButtonToggleChange } from "@angular/material/button-toggle"; +import { StarkRBACDirectivePermission } from "@nationalbankbelgium/stark-rbac"; + +@Component({ + selector: "demo-authorization-directives", + templateUrl: "./demo-authorization-directives-page.component.html", + styleUrls: ["./demo-authorization-directives-page.component.scss"] +}) +export class DemoAuthorizationDirectivesPageComponent { + public selectedAuthorizedRole: string = "manager"; // current user's role + + public get permissionsConfig(): StarkRBACDirectivePermission { + return { + roles: [this.selectedAuthorizedRole] + }; + } + + public onRoleChange(changeEvent: MatButtonToggleChange): void { + this.selectedAuthorizedRole = changeEvent.value; + } +} diff --git a/showcase/src/assets/examples/rbac-authorization-service/authorization-service.html b/showcase/src/assets/examples/rbac-authorization-service/authorization-service.html new file mode 100644 index 0000000000..c9ec1ae9a9 --- /dev/null +++ b/showcase/src/assets/examples/rbac-authorization-service/authorization-service.html @@ -0,0 +1,3 @@ + + + diff --git a/showcase/src/assets/translations/en.json b/showcase/src/assets/translations/en.json index 0a24c7f8ef..9e6005f605 100644 --- a/showcase/src/assets/translations/en.json +++ b/showcase/src/assets/translations/en.json @@ -330,6 +330,43 @@ "TITLE": "Toast notification" } }, + "DEMO_RBAC": { + "DIRECTIVES": { + "AUTHORIZATION": { + "HIDE_ON_PERMISSION": { + "TITLE": "Hide On Permission directive", + "SWITCH_UNAUTHORIZED_ROLE": "Switch the unauthorized role:", + "ONLY_VISIBLE_TO": "It is only visible to an authorized user that doesn't have the '{{ role }}' role." + }, + "SHOW_ON_PERMISSION": { + "TITLE": "Show On Permission directive", + "SWITCH_AUTHORIZED_ROLE": "Switch the authorized role:", + "ONLY_VISIBLE_TO": "It is only visible to an authorized user that has the '{{ role }}' role." + }, + "CONTENT_PROTECTED": "This content is protected.", + "TITLE": "Authorization directives" + } + }, + "SERVICES": { + "AUTHORIZATION": { + "FUNCTIONALITY": "Functionality", + "DESCRIPTION_METHODS": "The StarkRBACAuthorizationService provides the necessary methods to know whether the user has an specific role or set of roles and whether it is an anonymous user.", + "DESCRIPTION_TRANSITION_HOOKS": "It also adds a \"transition hook\" to the Router to protect certain routes from being accessed by unauthorized users. The transition hook will prevent the user from navigating to any route with specific permissions requirements defined on those routes. On top of that, the user can be redirected to another route if such redirection is defined as part of the route permissions configuration.", + "PROTECTED_ROUTES": "Declaring Protected Routes", + "DESCRIPTION_PROTECTED_ROUTES": "The protected routes should be declared using the StarkRBACStateDeclaration interface and defining the 'permissions' configuration inside the routes' 'data' object.
    In this example they are defined as follows:", + "NAVIGATE_TO_ADMINS_PAGE": "Go to Admins page", + "NAVIGATE_TO_MANAGERS_PAGE": "Go to Managers page", + "NAVIGATE_TO_SUPER_ADMINS_PAGE": "Go to Super-Admins page", + "REDIRECTION_MESSAGE": "{{ rbacActionType }}.
    You have been redirected because you don't have the right permissions to access the request page.", + "TITLE": "Authorization service" + }, + "RESTRICTED_PAGE": { + "TITLE": "Restricted Page", + "DESCRIPTION": "You have access to it because you have the right permissions.", + "GO_BACK": "Go back to previous page" + } + } + }, "GETTING_STARTED": { "TITLE": "Getting Started" }, @@ -338,7 +375,8 @@ "DESCRIPTION_DETAIL": "

    Stark provides blocks perfectly fit for accelerating front-end development.
    You'll find in Stark a reusable build based on Webpack, a starter project, core and ui modules providing awesome ui components, ...
    All those modules act like LEGO blocks: you add what you need, no more, no less!

    ", "DOCUMENTATION_CORE_DESC": "

    Stark Core, aka stark-core, is a module providing reusable APIs, such as a routing, logging, log shipping, etc.

    ", "DOCUMENTATION_MAIN_TITLE": "Documentation", - "DOCUMENTATION_SHOWCASE_DESC": "

    The showcase application is the application you are actually browsing right now!
    Enjoy roaming through our list of visual components (and their documentation) and have fun while discovering our design guidelines.

    ", + "DOCUMENTATION_RBAC_DESC": "

    Stark's RBAC module (aka stark-rbac) is a separate module in Stark that can be optionally included in any Stark based application in order to provide different elements\n(directives, services and components) to support Role Based Access Control (RBAC) mechanism.

    ", + "DOCUMENTATION_SHOWCASE_DESC": "

    The showcase application is the application you are actually browsing right now!
    Enjoy roaming through our list of visual components (and their documentation) and have fun while discovering our design guidelines.

    ", "DOCUMENTATION_UI_DESC": "

    Stark UI provides reusable UI components, for example data table, message pane, ... but also themes.

    ", "GETTING_STARTED": "Getting Started", "INTRODUCTION": "A front-end framework based on Angular 7", @@ -347,6 +385,7 @@ "MAIN_TITLE": "What is Stark?", "PREVIOUS_API": "Previous API", "PREVIOUS_VERSIONS": "Previous versions", + "RBAC": "Stark RBAC", "SHOWCASE": "Showcase", "TITLE": "Home", "UI": "Stark UI" @@ -354,7 +393,8 @@ "ICONS": { "GITHUB": "Github repo", "STARK_UI": "Stark-UI documentation", - "STARK_CORE": "Stark-Core documentation" + "STARK_CORE": "Stark-Core documentation", + "STARK_RBAC": "Stark-RBAC documentation" }, "NEWS": { "TITLE": "News" diff --git a/showcase/src/assets/translations/fr.json b/showcase/src/assets/translations/fr.json index 4f06dc80fc..4cce5891da 100644 --- a/showcase/src/assets/translations/fr.json +++ b/showcase/src/assets/translations/fr.json @@ -330,6 +330,43 @@ "TITLE": "Toast notification" } }, + "DEMO_RBAC": { + "DIRECTIVES": { + "AUTHORIZATION": { + "HIDE_ON_PERMISSION": { + "TITLE": "Hide On Permission directive", + "SWITCH_UNAUTHORIZED_ROLE": "Changer le rôle non autorisé:", + "ONLY_VISIBLE_TO": "Il n'est visible que par un utilisateur autorisé qui n'a pas le rôle '{{ role }}'." + }, + "SHOW_ON_PERMISSION": { + "TITLE": "Show On Permission directive", + "SWITCH_AUTHORIZED_ROLE": "Changer le rôle autorisé:", + "ONLY_VISIBLE_TO": "Il n'est visible que par un utilisateur autorisé ayant le rôle '{{ role }}'." + }, + "CONTENT_PROTECTED": "Ce contenu est protégé.", + "TITLE": "Authorization directives" + } + }, + "SERVICES": { + "AUTHORIZATION": { + "FUNCTIONALITY": "Fonctionnalité", + "DESCRIPTION_METHODS": "Le StarkRBACAuthorizationService fournit les méthodes nécessaires pour savoir si l'utilisateur a un rôle spécifique ou un ensemble de rôles ou s'il s'agit d'un utilisateur anonyme.", + "DESCRIPTION_TRANSITION_HOOKS": "Il ajoute également un \"transition hook\" au Routeur pour protéger certaines routes non accessibles à des utilisateurs non autorisés. Le \"transition hook\" empêchera l'utilisateur de naviguer vers n'importe quelle route avec des droits d'accès spécifiques définis sur celles-ci. En plus de cela, l'utilisateur peut être redirigé vers une autre route si cette redirection est définie dans la configuration des autorisations de la route.", + "PROTECTED_ROUTES": "Déclarer des routes protégées", + "DESCRIPTION_PROTECTED_ROUTES": "Les routes protégées doivent être déclarées à l'aide de l'interface StarkRBACStateDeclaration en définissant la configuration 'permissions' dans l'objet 'data' des routes.
    Dans cet exemple, elles sont définies comme suit:", + "NAVIGATE_TO_ADMINS_PAGE": "Aller sur la page des Admins", + "NAVIGATE_TO_MANAGERS_PAGE": "Aller sur la page des Managers", + "NAVIGATE_TO_SUPER_ADMINS_PAGE": "Aller sur la page des Super-Admins", + "REDIRECTION_MESSAGE": "{{ rbacActionType }}.
    Vous avez été redirigé car vous ne disposez pas des autorisations nécessaires pour accéder à la page demandée.", + "TITLE": "Authorization service" + }, + "RESTRICTED_PAGE": { + "TITLE": "Page restreinte", + "DESCRIPTION": "Vous y avez accès car vous disposez des autorisations adéquates.", + "GO_BACK": "Revenir à la page précédente" + } + } + }, "GETTING_STARTED": { "TITLE": "Démarrer" }, @@ -338,6 +375,7 @@ "DESCRIPTION_DETAIL": "

    Stark fournit les principaux outils pour accélérer le développement front-end.
    Vous y trouverez un build fiable et réutilisable basé sur Webpack, un projet Starter, des modules core et ui fournissant des composants ui incroyables, ...
    Tous ces modules sont comme des LEGO: ajoutez ce dont vous avez besoin, ni plus, ni moins!

    ", "DOCUMENTATION_CORE_DESC": "

    Stark Core, aussi appelé stark-core, est un module fournissant des APIs réutilisables, comme par exemple une API de routing, de logging, de log shipping,etc...

    ", "DOCUMENTATION_MAIN_TITLE": "Documentation", + "DOCUMENTATION_RBAC_DESC": "

    Le module RBAC de Stark, aussi appelé stark-rbac, est un module distinct de Stark qui peut éventuellement être inclus dans toute application basée sur Stark afin de fournir différents éléments (directives, services et composants) prenant en charge le mécanisme de contrôle d'accès basé sur le rôle (RBAC).

    ", "DOCUMENTATION_SHOWCASE_DESC": "

    Le Showcase, vous vous y trouvez en ce moment même!
    Amusez-vous à parcourir notre liste de composants visuels ainsi que leur documentation et à découvrir de vos propres yeux nos guidelines en matière de design.

    ", "DOCUMENTATION_UI_DESC": "

    Stark UI fournit des composants UI réutilisables: table de données, boite de dialogue, ... mais également des thèmes.

    ", "GETTING_STARTED": "Démarrer", @@ -347,6 +385,7 @@ "MAIN_TITLE": "Qu'est-ce que Stark?", "PREVIOUS_API": "API précédentes", "PREVIOUS_VERSIONS": "Versions précédentes", + "RBAC": "Stark RBAC", "SHOWCASE": "Showcase", "TITLE": "Home", "UI": "Stark UI" @@ -354,7 +393,8 @@ "ICONS": { "GITHUB": "Repo Github", "STARK_UI": "Documentation Stark-UI", - "STARK_CORE": "Documentation Stark-Core" + "STARK_CORE": "Documentation Stark-Core", + "STARK_RBAC": "Documentation Stark-RBAC" }, "NEWS": { "TITLE": "Nouvelles" diff --git a/showcase/src/assets/translations/nl.json b/showcase/src/assets/translations/nl.json index a70ea097a2..81ea129b5b 100644 --- a/showcase/src/assets/translations/nl.json +++ b/showcase/src/assets/translations/nl.json @@ -330,6 +330,43 @@ "TITLE": "Toast notification" } }, + "DEMO_RBAC": { + "DIRECTIVES": { + "AUTHORIZATION": { + "HIDE_ON_PERMISSION": { + "TITLE": "Hide On Permission directive", + "SWITCH_UNAUTHORIZED_ROLE": "Wissel van ongeautoriseerde rol:", + "ONLY_VISIBLE_TO": "Het is alleen zichtbaar voor een geautoriseerde gebruiker die niet de '{{ role }}' rol heeft." + }, + "SHOW_ON_PERMISSION": { + "TITLE": "Show On Permission directive", + "SWITCH_AUTHORIZED_ROLE": "Wissel van geautoriseerde rol: ", + "ONLY_VISIBLE_TO": "Het is alleen zichtbaar voor een geautoriseerde gebruiker die de '{{ role }}' rol heeft." + }, + "CONTENT_PROTECTED": "Deze inhoud is beveiligd.", + "TITLE": "Authorization directives" + } + }, + "SERVICES": { + "AUTHORIZATION": { + "FUNCTIONALITY": "Functionaliteit", + "DESCRIPTION_METHODS": "De StarkRBACAuthorizationService voorziet de nodige methodes om te weten of een gebruiker een (reeks van) specifieke rol(len) heeft of anoniem is.", + "DESCRIPTION_TRANSITION_HOOKS": "Het voegt ook een \"transition hook\" toe aan de Router om bepaalde routes te kunnen afschermen voor ongeautoriseerde gebruikers. De \"transition hook\" al de gebruiker voorkomen om eender welke route te gebruiken waarop specifieke requirements worden gedefinieerd. Daarbij kunnen gebruikers ook geredirect worden naar een andere route moest dit opgenomen zijn in de configuratie.", + "PROTECTED_ROUTES": "Beschermde routes declareren", + "DESCRIPTION_PROTECTED_ROUTES": "Beschermde routes moeten gedeclareerd worden door middel van de StarkRBACStateDeclaration interface en de definitie van de 'permissions' configuratie in het 'data' object van de route. In dit voorbeeld zijn ze als volgt gedefinieerd:", + "NAVIGATE_TO_ADMINS_PAGE": "Naar de Admin pagina", + "NAVIGATE_TO_MANAGERS_PAGE": "Naar de Manager pagina", + "NAVIGATE_TO_SUPER_ADMINS_PAGE": "Naar de Super-Admin pagina", + "REDIRECTION_MESSAGE": "{{ rbacActionType }}.
    U bent omgeleid omdat u niet over de juiste machtigingen beschikt om toegang te krijgen tot de aangevraagde pagina.", + "TITLE": "Authorization service" + }, + "RESTRICTED_PAGE": { + "TITLE": "Beschermde pagina", + "DESCRIPTION": "U hebt toegang omdat u de juiste rechten hebt.", + "GO_BACK": "Terug naar de vorige pagina" + } + } + }, "GETTING_STARTED": { "TITLE": "Begonnen" }, @@ -338,6 +375,7 @@ "DESCRIPTION_DETAIL": "

    Stark levert de belangrijkste instrumenten om de front-end ontwikkeling te versnellen.
    U zult een betrouwbare en herbruikbare build gebouwd op Webpack vinden, een Starter project, core en ui modules dat interessante visuele componenten verschaffen, ...
    Deze modules zijn zoals LEGO: Voegt wat u nodig hebt, niet meer en niet minder!

    ", "DOCUMENTATION_CORE_DESC": "

    Stark Core, ook bekend als stark-core, is een module dat herbruikbare APIs verstrekt, zoals routing, logging, log shipping,enzovoort.

    ", "DOCUMENTATION_MAIN_TITLE": "Documentatie", + "DOCUMENTATION_RBAC_DESC": "

    De RBAC-module van Stark (ook bekend als stark-rbac) is een afzonderlijke module in Stark die optioneel kan worden opgenomen in elke op Stark gebaseerde toepassing om verschillende elementen (directives, services en componenten) te bieden om Role Based Access Control (RBAC) mechanisme te ondersteunen.

    ", "DOCUMENTATION_SHOWCASE_DESC": "

    De Showcase, u zich hier bevindt!
    Veel plezier met de ontdekking van onze componenten lijst !

    ", "DOCUMENTATION_UI_DESC": "

    Stark UI levert UI herbruikbare componenten : gegevenstabel, dialoogvenster, ... maar ook themas.

    ", "GETTING_STARTED": "Ermee beginnen", @@ -347,6 +385,7 @@ "MAIN_TITLE": "Wat is Stark?", "PREVIOUS_API": "Vorige API", "PREVIOUS_VERSIONS": "Vorige versies", + "RBAC": "Stark RBAC", "SHOWCASE": "Showcase", "TITLE": "Home", "UI": "Stark UI" @@ -354,7 +393,8 @@ "ICONS": { "GITHUB": "Github repo", "STARK_UI": "Stark-UI documentatie", - "STARK_CORE": "Stark-Core documentatie" + "STARK_CORE": "Stark-Core documentatie", + "STARK_RBAC": "Stark-RBAC documentatie" }, "NEWS": { "TITLE": "Nieuws" diff --git a/showcase/src/styles/_theme.scss b/showcase/src/styles/_theme.scss index afd05a059c..e9a96f9319 100644 --- a/showcase/src/styles/_theme.scss +++ b/showcase/src/styles/_theme.scss @@ -10,7 +10,8 @@ Import the local variables file first to set the correct variables, see: @import "../app/shared/components/table-of-contents/table-of-contents-theme"; @import "../app/welcome/pages/getting-started/getting-started-page-theme"; @import "../app/welcome/pages/home/home-page-theme"; +@import "../app/welcome/pages/news/news-page-theme"; @import "../app/welcome/components/news-item/news-item-theme"; @import "../app/styleguide/pages/typography/styleguide-typography-page-theme"; -@import "../app/demo-ui/pages/route-search/demo-route-search-page.component-theme.scss"; -@import "../app/styleguide/pages/layout/styleguide-layout-page.theme.scss"; +@import "../app/demo-ui/pages/route-search/demo-route-search-page.component-theme"; +@import "../app/styleguide/pages/layout/styleguide-layout-page.theme";