Merge branch 'master' into new-eventstore

This commit is contained in:
adlerhurst
2020-10-27 16:07:24 +01:00
809 changed files with 48599 additions and 24100 deletions

View File

@@ -24,3 +24,11 @@ updates:
commit-message:
prefix: chore
include: scope
- package-ecosystem: npm
directory: "/site"
schedule:
interval: monthly
open-pull-requests-limit: 10
commit-message:
prefix: chore
include: scope

View File

@@ -3,9 +3,13 @@ name: "Code scanning - action"
on:
push:
branches: [master, ]
paths-ignore:
- 'site/**'
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
paths-ignore:
- 'site/**'
schedule:
- cron: '0 12 * * 2'

View File

@@ -47,4 +47,4 @@ jobs:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: gh-pages
FOLDER: site/__sapper__/export
CLEAN: true
CLEAN: true

View File

@@ -5,7 +5,7 @@ env:
GITHUB_TOKEN: ${{ secrets.CR_PAT }}
REGISTRY: ghcr.io
NODE_VERSION: '12'
GO_VERSION: '1.14'
GO_VERSION: '1.15'
jobs:
@@ -126,30 +126,6 @@ jobs:
repository: ${{ github.repository }}
tag_with_ref: true
tag_with_sha: true
container-vulnerability-scan:
runs-on: ubuntu-18.04
needs: container-prod
steps:
- name: Source checkout
uses: actions/checkout@v2
- name: Generate Short SHA Container Tag
id: vars
run: echo "::set-output name=sha_short::SHA-$(git rev-parse --short HEAD)"
- name: Check outputs
run: echo ${{ steps.vars.outputs.sha_short }}
- name: Docker Login
run: docker login $REGISTRY -u $GITHUB_ACTOR -p $GITHUB_TOKEN
- uses: anchore/scan-action@master
with:
image-reference: "${{ env.REGISTRY }}/${{ github.repository }}:${{ steps.vars.outputs.sha_short }}"
dockerfile-path: "./build/docker/Dockerfile"
fail-build: false
acs-report-enable: true
- name: Upload Anchore Scan Report
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: results.sarif
release:
runs-on: ubuntu-18.04
@@ -191,4 +167,4 @@ jobs:
if: env.CAOS_NEXT_VERSION != ''
- name: Docker Push Latest
run: docker push $REGISTRY/$GITHUB_REPOSITORY:latest
if: env.CAOS_NEXT_VERSION != ''
if: env.CAOS_NEXT_VERSION != ''

19
.github/workflows/spellcheck.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Spellcheck
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
spellcheck:
name: Typo CI (GitHub Action)
runs-on: ubuntu-latest
timeout-minutes: 4
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: TypoCheck
uses: typoci/spellcheck-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

40
.typo-ci.yml Normal file
View File

@@ -0,0 +1,40 @@
# What language dictionaries should it use? Currently Typo CI supports:
# de
# en
# en_GB
# es
# fr
# it
# pt
# pt_BR
dictionaries:
- en
- en_GB
- de
# Any files/folders we should ignore?
excluded_files:
- ".codecov/*"
- ".github/*"
- "build/*"
- "k8s/*"
- "*.min.css"
- "*.css.map"
- "*.min.js"
- "*.js.map"
- "package-lock.json"
- "package.json"
- ".releaserc.js"
- ".typo-ci.yml"
- ".gitignore"
- "go.mod"
- "go.sum"
# Any typos we should ignore?
excluded_words:
- typoci
- idps
- ZITADEL's
# Would you like filenames to also be spellchecked?
spellcheck_filenames: false

11
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,11 @@
# How to contribute to ZITADEL
## **Did you find a bug?**
## **Did you find a security flaw?**
* Please read [Security Policy](SECURITY.md).
## **Do you want to contribute to the ZITADEL documentation?**
* Please read [Contributing to the ZITADEL Documentation](site/CONTRIBUTING.md).

View File

@@ -1,4 +1,4 @@
<img src="./docs/img/zitadel-logo-dark@2x.png" alt="Zitadel Logo" height="100px" width="auto" />
<img src="./site/static/logos/zitadel-logo-dark@2x.png" alt="Zitadel Logo" height="100px" width="auto" />
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![Release](https://github.com/caos/zitadel/workflows/Release/badge.svg)](https://github.com/caos/zitadel/actions)
@@ -7,48 +7,49 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/caos/zitadel)](https://goreportcard.com/report/github.com/caos/zitadel)
[![codecov](https://codecov.io/gh/caos/zitadel/branch/master/graph/badge.svg)](https://codecov.io/gh/caos/zitadel)
> This project is in a alpha state. The application will continue breaking until version 1.0.0 is released
> This project is in a beta state and API might still change a bit
## What Is It
`ZITADEL` is a Cloud Native Identity and Access Management solution. All server side components are written in `Go` and the management interface, called `Console`, is written in `Angular`.
**ZITADEL** is a "Cloud Native Identity and Access Management" solution. All server side components are written in [**Go**](https://golang.org/) and the management interface, called **Console**, is written in [**Angular**](https://angular.io/).
We optimized `ZITADEL` for the usage as `service provider IAM`. By `service provider` we think of companies who build services for e.g SaaS cases. Often these companies would like to use an IAM where they can register their application and grant other people or companies the right to self manage a set of roles within that application.
We optimized **ZITADEL** for the usage as "service provider" IAM. By "service provider" we think of companies who build services for e.g SaaS cases. Often these companies would like to use an IAM where they can register their application and grant other people or companies the right to self manage a set of roles within that application.
## How Does It Work
We built `ZITADEL` around the idea that the IAM should be easy to deploy and scale. That's why we tried to reduce external systems as much as possible.
For example, `ZITADEL` is eventsourced but it does not rely on a pub/sub system to function. Instead we built all the functionality right into one binary.
`ZITADEL` only needs `Kubernetes` for orchestration and `CockroachDB` as storage.
We built **ZITADEL** around the idea that the IAM should be easy to deploy and scale. That's why we tried to reduce external systems as much as possible.
For example, **ZITADEL** is event sourced but it does not rely on a pub/sub system to function. Instead we built all the functionality right into one binary.
**ZITADEL** only needs [**Kubernetes**](https://kubernetes.io/) for orchestration and [**CockroachDB**](https://www.cockroachlabs.com/) as storage.
## Why Another IAM
In the past we already built a closed sourced IAM and tested multiple others. With most of them we had some issues, either technology, feature, pricing or transparency related in nature. For example we find the idea that security related features like `MFA` should not be hidden behind a paywall or a feature price.
One feature that we often missed, was a solid `audit trail` of all IAM resources. Most systems we saw so far either rely on simple log files or use a short retention for this.
In the past we already built a closed sourced IAM and tested multiple others. With most of them we had some issues, either technology, feature, pricing or transparency related in nature. For example we find the idea that security related features like **MFA** should not be hidden behind a paywall or a feature price.
One feature that we often missed, was a solid **audit trail** of all IAM resources. Most systems we saw so far either rely on simple log files or use a short retention for this.
## How To Use It
### Use our free tier
Stay tuned, we will publish how you can register an organisation in our cloud offering `zitadel.ch` soon.
Yes we have a free tier!
We provide a shared-cloud ZITADEL system where people can register there own organisation.
Until end of 2020 we operator under a **early access** model where everything is free.
Go check it out under [zitadel.ch](https://zitadel.ch)
### Run your own IAM
Stay tuned, we will soon publish a guide how you can deploy a `hyperconverged` system with our automation tooling called `ORBOS`.
With [ORBOS](https://github.com/caos/orbos/) you will be able to run `ZITADEL` on `GCE` or `StaticProvider` within 20 minutes. To achieve this, [ORBOS](https://github.com/caos/orbos/) will bootstrap and maintain a `Kubernetes` cluster, essential platform components (logging, metrics, ingress, ...), a secure `CockroachDB` cluster and `ZITADEL` itself.
Stay tuned, we will soon publish a guide how you can deploy a **hyperconverged** system with our automation tooling called [**ORBOS**](https://github.com/caos/orbos/).
With [**ORBOS**](https://github.com/caos/orbos/) you will be able to run [**Kubernetes**](https://kubernetes.io/) on **GCE** or **StaticProvider** within 20 minutes. To achieve this, [[**ORBOS**](https://github.com/caos/orbos/) will bootstrap and maintain a [**Kubernetes**](https://kubernetes.io/) cluster, essential platform components (logging, metrics, ingress, ...), a secure [**CockroachDB**](https://www.cockroachlabs.com/) cluster and **ZITADEL** itself.
The combination of the tools [ORBOS](https://github.com/caos/orbos/) and `ZITADEL` is what makes the operation easy and scalable.
See our progress [here](https://github.com/caos/orbos/pull/256)
The combination of the tools [**ORBOS**](https://github.com/caos/orbos/) and **ZITADEL** is what makes the operation easy and scalable.
## Give me some docs
This is work in progess but will change soon.
Have a look at our constantly evolving docs page [docs.zitadel.ch](https://docs.zitadel.ch).
## How To Contribute
TBA
Details need to be announced, but feel free to contribute already. As long as you are okay with accepting to contribute under this projects OSS [License](##License) you are fine.
We already have documentation specific [guidelines](./site/CONTRIBUTING.md).
## Security
@@ -59,3 +60,4 @@ See the policy [here](./SECURITY.md)
See the exact licensing terms [here](./LICENSE)
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@@ -1,5 +0,0 @@
# Build
## Console
## Docker

View File

@@ -23,52 +23,75 @@ Eventstore:
MaxCacheSizeInByte: 10485760 #10mb
SetUp:
GlobalOrg: 'Global'
IAMProject: 'Zitadel'
DefaultLoginPolicy:
AllowUsernamePassword: true
AllowRegister: true
AllowExternalIdp: true
Orgs:
- Name: 'Global'
Domain: 'global.caos.ch'
Default: true
OrgIamPolicy: true
Users:
- FirstName: 'Global Org'
LastName: 'Administrator'
UserName: 'zitadel-global-org-admin@caos.ch'
Email: 'zitadel-global-org-admin@caos.ch'
Password: 'Password1!'
Owners:
- 'zitadel-global-org-admin@caos.ch'
- Name: 'CAOS AG'
Domain: 'caos.ch'
Users:
- FirstName: 'Zitadel'
LastName: 'Administrator'
UserName: 'zitadel-admin'
Email: 'zitadel-admin@caos.ch'
Password: 'Password1!'
Owners:
- 'zitadel-admin@caos.ch'
Projects:
- Name: 'Zitadel'
OIDCApps:
- Name: 'Management-API'
- Name: 'Auth-API'
- Name: 'Admin-API'
- Name: 'Zitadel Console'
RedirectUris:
- '$ZITADEL_CONSOLE/auth/callback'
PostLogoutRedirectUris:
- '$ZITADEL_CONSOLE/signedout'
ResponseTypes:
- $ZITADEL_CONSOLE_RESPONSE_TYPE
GrantTypes:
- $ZITADEL_CONSOLE_GRANT_TYPE
ApplicationType: 'USER_AGENT'
AuthMethodType: 'NONE'
DevMode: $ZITADEL_CONSOLE_DEV_MODE
Owners:
- 'zitadel-admin@caos.ch'
Step1:
GlobalOrg: 'Global'
IAMProject: 'Zitadel'
DefaultLoginPolicy:
AllowUsernamePassword: true
AllowRegister: true
AllowExternalIdp: true
Orgs:
- Name: 'Global'
Domain: 'global.caos.ch'
Default: true
OrgIamPolicy: true
Users:
- FirstName: 'Global Org'
LastName: 'Administrator'
UserName: 'zitadel-global-org-admin@caos.ch'
Email: 'zitadel-global-org-admin@caos.ch'
Password: 'Password1!'
Owners:
- 'zitadel-global-org-admin@caos.ch'
- Name: 'CAOS AG'
Domain: 'caos.ch'
Users:
- FirstName: 'Zitadel'
LastName: 'Administrator'
UserName: 'zitadel-admin'
Email: 'zitadel-admin@caos.ch'
Password: 'Password1!'
Owners:
- 'zitadel-admin@caos.ch'
Projects:
- Name: 'Zitadel'
OIDCApps:
- Name: 'Management-API'
- Name: 'Auth-API'
- Name: 'Admin-API'
- Name: 'Zitadel Console'
RedirectUris:
- '$ZITADEL_CONSOLE/auth/callback'
PostLogoutRedirectUris:
- '$ZITADEL_CONSOLE/signedout'
ResponseTypes:
- $ZITADEL_CONSOLE_RESPONSE_TYPE
GrantTypes:
- $ZITADEL_CONSOLE_GRANT_TYPE
ApplicationType: 'USER_AGENT'
AuthMethodType: 'NONE'
DevMode: $ZITADEL_CONSOLE_DEV_MODE
Owners:
- 'zitadel-admin@caos.ch'
Step2:
DefaultPasswordComplexityPolicy:
MinLength: 8
HasLowercase: true
HasUppercase: true
HasSymbol: true
HasNumber: true
Step3:
DefaultPasswordAgePolicy:
MaxAgeDays: 0
ExpireWarnDays: 0
Step4:
DefaultPasswordLockoutPolicy:
MaxAttempts: 5
ShowLockOutFailures: false
Step5:
DefaultOrgIAMPolicy:
UserLoginMustBeDomain: true
Step6:
DefaultLabelPolicy:
PrimaryColor: '#222324'
SecondaryColor: '#ffffff'

View File

@@ -52,28 +52,10 @@ SystemDefaults:
EncryptionKeyID: $ZITADEL_OTP_VERIFICATION_KEY
VerificationLifetimes:
PasswordCheck: 240h #10d
ExternalLoginCheck: 240h #10d
MfaInitSkip: 720h #30d
MfaSoftwareCheck: 18h
MfaHardwareCheck: 12h
DefaultPolicies:
Age:
Description: Standard age policy
MaxAgeDays: 365
ExpireWarnDays: 10
Complexity:
Description: Standard complexity policy
MinLength: 8
HasLowercase: true
HasUppercase: false
HasNumber: true
HasSymbol: true
Lockout:
Description: Standard lockout policy
MaxAttempts: 5
ShowLockOutFailures: true
OrgIam:
Description: Standard org policy
UserLoginMustBeDomain: true
IamID: 'IAM'
DomainVerification:
VerificationKey:

View File

@@ -26,7 +26,7 @@
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest",
"src/manifest.webmanifest"
],
"styles": [
"src/styles.scss"
@@ -34,8 +34,9 @@
"scripts": [],
"allowedCommonJsDependencies": [
"@angular/common/locales/de",
"src/app/proto/generated/*.js",
"src/app/proto/generated/**/*.js"
"src/app/proto/generated/**",
"file-saver",
"qrcode"
]
},
"configurations": {

3593
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@
"@types/file-saver": "^2.0.1",
"@types/uuid": "^8.3.0",
"@types/google-protobuf": "^3.7.3",
"angularx-qrcode": "^10.0.10",
"angularx-qrcode": "^10.0.11",
"angular-oauth2-oidc": "^10.0.3",
"cors": "^2.8.5",
"file-saver": "^2.0.2",
@@ -35,34 +35,34 @@
"google-protobuf": "^3.13.0",
"grpc": "^1.24.3",
"grpc-web": "^1.2.1",
"moment": "^2.27.0",
"moment": "^2.29.1",
"ngx-moment": "^5.0.0",
"ngx-quicklink": "^0.2.4",
"rxjs": "~6.6.3",
"ts-protoc-gen": "^0.12.0",
"tslib": "^2.0.1",
"uuid": "^8.3.0",
"zone.js": "~0.11.1"
"ts-protoc-gen": "^0.13.0",
"tslib": "^2.0.3",
"uuid": "^8.3.1",
"zone.js": "~0.11.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1000.8",
"@angular/cli": "~10.0.7",
"@angular-devkit/build-angular": "~0.1002.0",
"@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.0.11",
"@types/jasmine": "~3.5.13",
"@angular/language-service": "~10.1.0",
"@types/jasmine": "~3.6.0",
"@angular/language-service": "~10.2.0",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^14.6.4",
"codelyzer": "^6.0.0",
"@types/node": "^14.14.3",
"codelyzer": "^6.0.1",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.2.1",
"jasmine-spec-reporter": "~6.0.0",
"karma": "~5.2.3",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.1",
"karma-jasmine-html-reporter": "^1.5.0",
"prettier": "^2.1.1",
"prettier": "^2.1.2",
"protractor": "~7.0.0",
"stylelint": "^13.7.1",
"stylelint": "^13.7.2",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
"ts-node": "~9.0.0",

View File

@@ -77,6 +77,38 @@ export const navAnimations: Array<AnimationTriggerMetadata> = [
]),
];
export const enterAnimations: Array<AnimationTriggerMetadata> = [
trigger('appearfade', [
transition(':enter', [
style({
transform: 'scale(.9) translateY(-10%)',
opacity: 0,
}),
animate(
'100ms ease-in-out',
style({
transform: 'scale(1) translateY(0%)',
opacity: 1,
}),
),
]),
transition(':leave', [
style({
transform: 'scale(1) translateY(0%)',
opacity: 1,
}),
animate(
'100ms ease-in-out',
style({
transform: 'scale(.9) translateY(-10%)',
opacity: 0,
}),
),
]),
]),
];
export const routeAnimations: AnimationTriggerMetadata = trigger('routeAnimations', [
transition('HomePage => AddPage', [
style({ transform: 'translateX(100%)', opacity: 0.5 }),

View File

@@ -65,6 +65,14 @@ const routes: Routes = [
roles: ['org.read'],
},
},
{
path: 'grants',
loadChildren: () => import('./pages/grants/grants.module').then(m => m.GrantsModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['user.grant.read'],
},
},
{
path: 'grant-create',
canActivate: [AuthGuard],
@@ -87,6 +95,24 @@ const routes: Routes = [
roles: ['user.grant.write'],
},
},
{
path: 'user/:userid',
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module')
.then(m => m.UserGrantCreateModule),
canActivate: [RoleGuard],
data: {
roles: ['user.grant.write'],
},
},
{
path: '',
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module')
.then(m => m.UserGrantCreateModule),
canActivate: [RoleGuard],
data: {
roles: ['user.grant.write'],
},
},
],
},
{

View File

@@ -1,5 +1,5 @@
<ng-container *ngIf="(authService.user | async) || {} as user">
<ng-container *ngIf="((['iam.read','iam.write'] | hasRole)) as iamuser$">
<ng-container *ngIf="((['iam.read$','iam.write$'] | hasRole)) as iamuser$">
<mat-toolbar class="root-header">
<button aria-label="Toggle sidenav" mat-icon-button (click)="drawer.toggle()">
<i class="icon las la-bars"></i>
@@ -12,22 +12,31 @@
</ng-template>
</a>
<button (click)="loadOrgs()" *ngIf="profile?.id && org" mat-button
[matMenuTriggerFor]="menu">{{org?.name ? org.name : 'NO NAME'}}
<button (click)="loadOrgs()" *ngIf="profile?.id && org" mat-button [matMenuTriggerFor]="menu"
(menuOpened)="focusFilter()">{{org?.name ? org.name : 'NO NAME'}}
<mat-icon>
arrow_drop_down</mat-icon>
</button>
<mat-menu #menu="matMenu">
<mat-progress-bar *ngIf="orgLoading" color="accent" mode="indeterminate"></mat-progress-bar>
<button class="show-all" mat-menu-item
[routerLink]="[ '/org/overview' ]">{{'MENU.SHOWORGS' | translate}}</button>
<mat-menu class="menu" #menu="matMenu">
<div class="spinner-w">
<mat-spinner diameter="20" *ngIf="orgLoading$ | async" color="accent">
</mat-spinner>
</div>
<mat-form-field class="filter-form" appearance="fill">
<input matInput [formControl]="filterControl" autocomplete="off" (click)="$event.stopPropagation()"
placeholder="{{'ORG.PAGES.FILTERPLACEHOLDER' | translate}}" #input>
</mat-form-field>
<button [ngClass]="{'active': temporg.id === org?.id}" [disabled]="!temporg.id"
*ngFor="let temporg of orgs" mat-menu-item (click)="setActiveOrg(temporg)">
*ngFor="let temporg of orgs$ | async" mat-menu-item (click)="setActiveOrg(temporg)">
{{temporg?.name ? temporg.name : 'NO NAME'}}
</button>
<button class="show-all" mat-menu-item
[routerLink]="[ '/org/overview' ]">{{'MENU.SHOWORGS' | translate}}</button>
<ng-template appHasRole [appHasRole]="['org.create','iam.write']">
<button mat-menu-item [routerLink]="[ '/org/create' ]">
<mat-icon class="avatar">add</mat-icon>
@@ -129,6 +138,21 @@
<span class="label">{{ 'MENU.MACHINEUSERS' | translate }}</span>
</a>
</ng-template>
<ng-template appHasRole [appHasRole]="['user.grant.read(:[0-9]*)?']">
<div @navitem class="divider">
<div class="line"></div>
<span class="label">
{{ 'MENU.GRANTSECTION' | translate }}</span>
<div class="line"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/grants']"
[routerLinkActiveOptions]="{ exact: true }">
<i class="icon las la-shield-alt"></i>
<span class="label">{{ 'MENU.GRANTS' | translate }}</span>
</a>
</ng-template>
</div>
<span class="fill-space"></span>

View File

@@ -1,3 +1,4 @@
@import '~@angular/material/theming';
.root-header {
position: fixed;
@@ -159,13 +160,6 @@
margin-bottom: 1rem;
}
}
.primary-button {
margin: 1rem;
border-radius: 1.5rem;
height: 2.5rem;
padding: 0 1rem;
}
}
.content {
@@ -174,7 +168,7 @@
.router {
height: 100%;
overflow: auto;
overflow-y: auto;
}
}
@@ -214,18 +208,50 @@
margin: .5rem 0;
span {
border: 1px solid #ffffff10;
border: 1px solid #81868a40;
padding: 2px 1rem;
border-radius: 50vw;
color: #8795a1;
font-size: 12px;
color: var(--grey);
font-size: 11px;
}
.line {
display: block;
background-color: #ffffff10;
background-color: #81868a40;
height: 1px;
margin: .5rem 0;
flex: 1;
}
}
@mixin textvar($theme) {
.filter-form {
margin: 0 .5rem;
/* stylelint-disable */
$foreground: map-get($theme, foreground);
color: mat-color($foreground, text) !important;
}
.show-all {
$primary: map-get($theme, primary);
color: mat-color($primary, 300) !important;
border-bottom: 2px solid var(--grey);
margin-bottom: .5rem;
}
/* stylelint-enable */
}
.menu {
position: relative;
.spinner-w {
top: 1rem;
left: 0;
right: 0;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
}

View File

@@ -1,17 +1,24 @@
import { BreakpointObserver } from '@angular/cdk/layout';
import { OverlayContainer } from '@angular/cdk/overlay';
import { ViewportScroller } from '@angular/common';
import { Component, HostBinding, Inject, OnDestroy, ViewChild } from '@angular/core';
import { DOCUMENT, ViewportScroller } from '@angular/common';
import { Component, ElementRef, HostBinding, Inject, OnDestroy, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatIconRegistry } from '@angular/material/icon';
import { MatDrawer } from '@angular/material/sidenav';
import { DomSanitizer } from '@angular/platform-browser';
import { Router, RouterOutlet } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs';
import { catchError, debounceTime, finalize, map, take } from 'rxjs/operators';
import { accountCard, navAnimations, routeAnimations, toolbarAnimation } from './animations';
import { Org, UserProfileView } from './proto/generated/auth_pb';
import {
MyProjectOrgSearchKey,
MyProjectOrgSearchQuery,
Org,
SearchMethod,
UserProfileView,
} from './proto/generated/auth_pb';
import { AuthenticationService } from './services/authentication.service';
import { GrpcAuthService } from './services/grpc-auth.service';
import { ManagementService } from './services/mgmt.service';
@@ -32,6 +39,7 @@ import { UpdateService } from './services/update.service';
})
export class AppComponent implements OnDestroy {
@ViewChild('drawer') public drawer!: MatDrawer;
@ViewChild('input', { static: false }) input!: ElementRef;
public isHandset$: Observable<boolean> = this.breakpointObserver
.observe('(max-width: 599px)')
.pipe(map(result => {
@@ -41,17 +49,18 @@ export class AppComponent implements OnDestroy {
public showAccount: boolean = false;
public org!: Org.AsObject;
public orgs: Org.AsObject[] = [];
public orgs$: Observable<Org.AsObject[]> = of([]);
public profile!: UserProfileView.AsObject;
public isDarkTheme: Observable<boolean> = of(true);
public orgLoading: boolean = false;
public orgLoading$: BehaviorSubject<any> = new BehaviorSubject(false);
public showProjectSection: boolean = false;
public grantedProjectsCount: number = 0;
public ownedProjectsCount: number = 0;
public filterControl: FormControl = new FormControl('');
private authSub: Subscription = new Subscription();
private orgSub: Subscription = new Subscription();
@@ -70,6 +79,7 @@ export class AppComponent implements OnDestroy {
private toast: ToastService,
private router: Router,
update: UpdateService,
@Inject(DOCUMENT) private document: Document,
) {
console.log('%cWait!', 'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5282c1; font-size: 50px');
console.log('%cInserting something here could give attackers access to your zitadel account.', 'color: red; font-size: 18px');
@@ -140,7 +150,6 @@ export class AppComponent implements OnDestroy {
this.orgSub = this.authService.activeOrgChanged.subscribe(org => {
this.org = org;
this.getProjectCount();
});
@@ -160,6 +169,16 @@ export class AppComponent implements OnDestroy {
this.isDarkTheme = this.themeService.isDarkTheme;
this.isDarkTheme.subscribe(thema => this.onSetTheme(thema ? 'dark-theme' : 'light-theme'));
this.translate.onLangChange.subscribe((language: LangChangeEvent) => {
this.document.documentElement.lang = language.lang;
});
this.filterControl.valueChanges.pipe(debounceTime(300)).subscribe(value => {
this.loadOrgs(
value.trim().toLowerCase(),
);
});
}
public ngOnDestroy(): void {
@@ -167,15 +186,26 @@ export class AppComponent implements OnDestroy {
this.orgSub.unsubscribe();
}
public loadOrgs(): void {
this.orgLoading = true;
this.authService.SearchMyProjectOrgs(10, 0).then(res => {
this.orgs = res.toObject().resultList;
this.orgLoading = false;
}).catch(error => {
this.toast.showError(error);
this.orgLoading = false;
});
public loadOrgs(filter?: string): void {
let query;
if (filter) {
query = new MyProjectOrgSearchQuery();
query.setMethod(SearchMethod.SEARCHMETHOD_CONTAINS_IGNORE_CASE);
query.setKey(MyProjectOrgSearchKey.MYPROJECTORGSEARCHKEY_ORG_NAME);
query.setValue(filter);
}
this.orgLoading$.next(true);
this.orgs$ = from(this.authService.SearchMyProjectOrgs(10, 0, query ? [query] : undefined)).pipe(
map(resp => {
return resp.toObject().resultList;
}),
catchError(() => of([])),
finalize(() => {
this.orgLoading$.next(false);
this.focusFilter();
}),
);
}
public prepareRoute(outlet: RouterOutlet): boolean {
@@ -200,19 +230,24 @@ export class AppComponent implements OnDestroy {
this.authService.user.subscribe(userprofile => {
this.profile = userprofile;
const lang = userprofile.preferredLanguage.match(/en|de/) ? userprofile.preferredLanguage : 'en';
const cropped = navigator.language.split('-')[0] ?? 'en';
const fallbackLang = cropped.match(/en|de/) ? cropped : 'en';
const lang = userprofile.preferredLanguage.match(/en|de/) ? userprofile.preferredLanguage : fallbackLang;
this.translate.use(lang);
this.document.documentElement.lang = lang;
});
}
public setActiveOrg(org: Org.AsObject): void {
this.org = org;
this.authService.setActiveOrg(org);
this.router.navigate(['/']);
this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => {
this.router.navigate(['/']);
});
}
private getProjectCount(): void {
this.authService.isAllowed(['project.read']).subscribe((allowed) => {
this.authService.isAllowed(['project.read$']).subscribe((allowed) => {
if (allowed) {
this.mgmtService.SearchProjects(0, 0).then(res => {
this.ownedProjectsCount = res.toObject().totalResult;
@@ -224,5 +259,11 @@ export class AppComponent implements OnDestroy {
}
});
}
focusFilter(): void {
setTimeout(() => {
this.input.nativeElement.focus();
}, 0);
}
}

View File

@@ -3,13 +3,17 @@ import { CommonModule, registerLocaleData } from '@angular/common';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import localeDe from '@angular/common/locales/de';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatNativeDateModule } from '@angular/material/core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
@@ -21,7 +25,7 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { QuicklinkModule } from 'ngx-quicklink';
import { RegExpPipeModule } from 'src/app/pipes/regexp-pipe.module';
import { RegExpPipeModule } from 'src/app/pipes/regexp-pipe/regexp-pipe.module';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
@@ -32,13 +36,15 @@ import { AccountsCardModule } from './modules/accounts-card/accounts-card.module
import { AvatarModule } from './modules/avatar/avatar.module';
import { WarnDialogModule } from './modules/warn-dialog/warn-dialog.module';
import { SignedoutComponent } from './pages/signedout/signedout.component';
import { HasRolePipeModule } from './pipes/has-role-pipe.module';
import { HasRolePipeModule } from './pipes/has-role-pipe/has-role-pipe.module';
import { GrpcAuthService } from './services/grpc-auth.service';
import { GrpcService } from './services/grpc.service';
import { AuthInterceptor } from './services/interceptors/auth.interceptor';
import { GRPC_INTERCEPTORS } from './services/interceptors/grpc-interceptor';
import { I18nInterceptor } from './services/interceptors/i18n.interceptor';
import { OrgInterceptor } from './services/interceptors/org.interceptor';
import { RefreshService } from './services/refresh.service';
import { SeoService } from './services/seo.service';
import { StatehandlerProcessorService, StatehandlerProcessorServiceImpl } from './services/statehandler-processor.service';
import { StatehandlerService, StatehandlerServiceImpl } from './services/statehandler.service';
import { StorageService } from './services/storage.service';
@@ -104,9 +110,13 @@ const authConfig: AuthConfig = {
MatSidenavModule,
MatCardModule,
OutsideClickModule,
MatFormFieldModule,
MatInputModule,
HasRolePipeModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatToolbarModule,
ReactiveFormsModule,
MatMenuModule,
MatSnackBarModule,
AvatarModule,
@@ -150,11 +160,17 @@ const authConfig: AuthConfig = {
multi: true,
useClass: AuthInterceptor,
},
{
provide: GRPC_INTERCEPTORS,
multi: true,
useClass: I18nInterceptor,
},
{
provide: GRPC_INTERCEPTORS,
multi: true,
useClass: OrgInterceptor,
},
SeoService,
RefreshService,
GrpcService,
GrpcAuthService,

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { filter, first, switchMap } from 'rxjs/operators';
import { GrpcAuthService } from '../services/grpc-auth.service';
@@ -15,6 +16,11 @@ export class RoleGuard implements CanActivate {
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> {
return this.authService.isAllowed(route.data['roles']);
return this.authService.fetchedZitadelPermissions.pipe(
filter((permissionsFetched) => !!permissionsFetched),
first(),
).pipe(
switchMap(_ => this.authService.isAllowed(route.data['roles'])),
);
}
}

View File

@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { GrpcAuthService } from '../services/grpc-auth.service';
@Injectable({
providedIn: 'root',
})
export class UserGuard implements CanActivate {
constructor(private authService: GrpcAuthService, private router: Router) { }
public canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> | Promise<boolean> | boolean {
return this.authService.user.pipe(
map(user => user.id !== route.params.id),
tap((isNotMe) => {
if (!isNotMe) {
this.router.navigate(['/users', 'me']);
}
}),
);
}
}

View File

@@ -16,7 +16,7 @@
</app-avatar>
<div class="col">
<span class="title">{{user.displayName ? user.displayName : user.userName}} </span>
<span class="user-title">{{user.displayName ? user.displayName : user.userName}} </span>
<span class="loginname">{{user.loginName}}</span>
<span class="email">{{'USER.STATE.'+user.authState | translate}}</span>
</div>
@@ -28,7 +28,7 @@
<i class="las la-user-plus"></i>
</div>
<span class="col">
<span class="title">{{'USER.ADDACCOUNT' | translate}}</span>
<span class="user-title">{{'USER.ADDACCOUNT' | translate}}</span>
</span>
<span class="fill-space"></span>
<mat-icon>keyboard_arrow_right</mat-icon>

View File

@@ -41,9 +41,11 @@
display: flex;
flex-direction: column;
width: 100%;
border-top: 1px solid #ffffff30;
border-bottom: 1px solid #ffffff30;
padding: .5rem 0;
max-height: 310px;
overflow-y: auto;
border-top: 1px solid rgba(#8795a1, .3);
border-bottom: 1px solid rgba(#8795a1, .3);
.row {
padding: .5rem;
@@ -84,7 +86,7 @@
display: flex;
flex-direction: column;
.title {
.user-title {
font-weight: 500;
font-size: .9rem;
line-height: 1rem;
@@ -92,7 +94,7 @@
.email,
.loginname {
color: #8795a1;
color: var(--grey);
font-size: .8rem;
line-height: 1rem;
}

View File

@@ -21,7 +21,9 @@ export class AccountsCardComponent implements OnInit {
this.userService.getMyUserSessions().then(sessions => {
this.users = sessions.toObject().userSessionsList;
const index = this.users.findIndex(user => user.loginName === this.profile.preferredLoginName);
this.users.splice(index, 1);
if (index > -1) {
this.users.splice(index, 1);
}
this.loadingUsers = false;
}).catch(() => {

View File

@@ -9,8 +9,9 @@
<mat-form-field class="full-width" appearance="outline">
<mat-label>{{ 'MEMBER.CREATIONTYPE' | translate }}</mat-label>
<mat-select [(ngModel)]="creationType" (selectionChange)="loadRoles()">
<mat-option *ngFor="let type of creationTypes" [value]="type">
{{ 'MEMBER.CREATIONTYPES.'+type | translate}}
<mat-option *ngFor="let type of creationTypes" [value]="type.type"
[disabled]="(type.disabled$ | async) == false">
{{ 'MEMBER.CREATIONTYPES.'+type.type | translate}}
</mat-option>
</mat-select>
</mat-form-field>

View File

@@ -3,7 +3,7 @@
}
.desc {
color: #8795a1;
color: var(--grey);
font-size: .9rem;
}
@@ -18,8 +18,4 @@
.ok-button {
margin-left: .5rem;
}
button {
border-radius: .5rem;
}
}

View File

@@ -1,7 +1,9 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Observable } from 'rxjs';
import { ProjectGrantView, ProjectRole, ProjectView, UserView } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -23,13 +25,18 @@ export class MemberCreateDialogComponent {
private grantId: string = '';
public preselectedUsers: Array<UserView.AsObject> = [];
public creationType!: CreationType;
public creationTypes: CreationType[] = [
CreationType.IAM,
CreationType.ORG,
CreationType.PROJECT_OWNED,
CreationType.PROJECT_GRANTED,
/**
* Specifies options for creating members,
* without ending $, to enable write event permission even if user is allowed
* to create members for only one specific project.
*/
public creationTypes: Array<{ type: CreationType, disabled$: Observable<boolean>; }> = [
{ type: CreationType.IAM, disabled$: this.authService.isAllowed(['iam.member.write$']) },
{ type: CreationType.ORG, disabled$: this.authService.isAllowed(['org.member.write$']) },
{ type: CreationType.PROJECT_OWNED, disabled$: this.authService.isAllowed(['project.member.write']) },
{ type: CreationType.PROJECT_GRANTED, disabled$: this.authService.isAllowed(['project.grant.member.write']) },
];
public users: Array<UserView.AsObject> = [];
public roles: Array<ProjectRole.AsObject> | string[] = [];
@@ -41,6 +48,7 @@ export class MemberCreateDialogComponent {
constructor(
private mgmtService: ManagementService,
private adminService: AdminService,
private authService: GrpcAuthService,
public dialogRef: MatDialogRef<MemberCreateDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private toastService: ToastService,

View File

@@ -1,9 +1,11 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { TranslateModule } from '@ngx-translate/core';
import { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.module';
@@ -21,10 +23,13 @@ import { MemberCreateDialogComponent } from './member-create-dialog.component';
CommonModule,
MatDialogModule,
MatButtonModule,
MatChipsModule,
MatInputModule,
TranslateModule,
MatFormFieldModule,
MatSelectModule,
FormsModule,
ReactiveFormsModule,
SearchUserAutocompleteModule,
SearchRolesAutocompleteModule,
SearchProjectAutocompleteModule,

View File

@@ -34,7 +34,7 @@
.desc {
font-size: .9rem;
color: #8795a1;
color: var(--grey);
}
}

View File

@@ -11,7 +11,7 @@
.card {
background-color: $primary-dark;
transition: background-color .4s ease-in-out;
transition: background-color .3s cubic-bezier(.645, .045, .355, 1);
border: 1px solid rgba($border-color, .2);
box-sizing: border-box;
border-radius: .5rem;

View File

@@ -21,13 +21,13 @@
flex-direction: column;
.editor {
color: #8795a1;
color: var(--grey);
font-size: 12px;
align-self: flex-end;
}
.seq {
color: #8795a1;
color: var(--grey);
font-size: 12px;
align-self: flex-end;
}
@@ -43,7 +43,7 @@
&.change-item-back {
background-color: rgba($primary-dark, .93);
transition: background-color .4s ease-in-out;
transition: background-color .3s cubic-bezier(.645, .045, .355, 1);
}
}
@@ -55,7 +55,7 @@
.end-container {
font-size: 12px;
color: #8795a1;
color: var(--grey);
}
}
}

View File

@@ -103,11 +103,9 @@ export class ChangesComponent implements OnInit {
this._loading.next(true);
return from(col).pipe(
take(1),
tap((res: Changes) => {
let values = res.toObject().changesList;
// If prepending, reverse the batch order
values = false ? values.reverse() : values;
const values = res.toObject().changesList;
// update source with new values, done loading
this._data.next(values);
@@ -118,12 +116,11 @@ export class ChangesComponent implements OnInit {
this._done.next(true);
}
}),
catchError(err => {
catchError(_ => {
this._loading.next(false);
this.bottom = true;
return of([]);
}),
take(1),
).subscribe();
}
}

View File

@@ -4,9 +4,9 @@ import { NgModule } from '@angular/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TranslateModule } from '@ngx-translate/core';
import { ScrollableModule } from 'src/app/directives/scrollable/scrollable.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { ChangesComponent } from './changes.component';

View File

@@ -8,13 +8,19 @@
<ng-container *ngIf="totalResult < 10; else compact">
<ng-container *ngFor="let member of membersSubject | async; index as i">
<div @animate (click)="emitShowDetail()" class="avatar-circle"
matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}"
matTooltip="{{ member.displayName }} | {{member.rolesList?.join(' ')}}"
[ngStyle]="{'z-index': 100 - i}">
<app-avatar *ngIf="member && (member.displayName || (member.firstName && member.lastName))"
<app-avatar
*ngIf="member && member.displayName && member.firstName && member.lastName; else cog"
class="avatar dontcloseonclick"
[name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)"
[size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon">
<i class="las la-user-cog"></i>
</div>
</ng-template>
</div>
</ng-container>
</ng-container>

View File

@@ -9,7 +9,7 @@
.sub-header {
font-size: .8rem;
color: #8795a1;
color: var(--grey);
}
.people {
@@ -65,6 +65,17 @@
.avatar {
pointer-events: none;
}
.sa-icon {
display: block;
width: 32px;
margin: 0 .5rem;
i {
margin: auto;
font-size: 1.2rem;
}
}
}
.margin-neg {

View File

@@ -1,14 +1,16 @@
<div class="max-width-container detail-container">
<div class="detail-left">
<a *ngIf="backRouterLink" [routerLink]="backRouterLink" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
</div>
<div class="detail-right">
<div class="head">
<h1>{{ title }}</h1>
<p class="desc">{{ description }}</p>
<ng-content></ng-content>
<div class="max-width-container">
<div class="detail-container">
<div class="detail-left">
<a *ngIf="backRouterLink" [routerLink]="backRouterLink" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
</div>
<div class="detail-right">
<div class="head">
<h1>{{ title }}</h1>
<p class="desc">{{ description }}</p>
<ng-content></ng-content>
</div>
</div>
</div>
</div>

View File

@@ -10,9 +10,9 @@
$lighter-color: rgba(mat-color($primary, 300), .5);
.detail-container {
width: 100%;
display: flex;
padding-bottom: 3rem;
padding-top: 3rem;
.detail-left {
width: 100px;
@@ -29,7 +29,6 @@
.detail-right {
flex: 1;
position: relative;
padding-left: 1rem;
@media only screen and (max-width: 500px) {
@@ -37,20 +36,17 @@
}
.head {
display: flex;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
h1 {
font-size: 1.2rem;
font-size: 1.5rem;
margin-top: 10px;
}
.desc {
width: 100%;
display: block;
font-size: .9rem;
color: #8795a1;
color: var(--grey);
}
}
}

View File

@@ -11,6 +11,8 @@
<h1>{{'IDP.CREATE.TITLE' | translate}}</h1>
<p>{{'IDP.CREATE.DESCRIPTION' | translate}}</p>
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<form (ngSubmit)="addIdp()">
<ng-container [formGroup]="formGroup">
<div class="content">
@@ -48,22 +50,22 @@
</mat-form-field>
</div>
<div class="content">
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</mat-label>
<mat-select formControlName="idpDisplayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINTFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</mat-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINTFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</mat-label>
<mat-select formControlName="idpDisplayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINTFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</mat-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINTFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-container>
@@ -71,4 +73,4 @@
{{ 'ACTIONS.SAVE' | translate }}
</button>
</form>
</div>
</div>

View File

@@ -25,7 +25,6 @@
.add-line-btn {
margin-bottom: 1rem;
border-radius: .5rem;
}
}
@@ -38,7 +37,7 @@
flex-basis: 100%;
margin: 0 .5rem;
margin-bottom: 1rem;
color: #8795a1;
color: var(--grey);
}
.formfield {
@@ -55,7 +54,6 @@
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
border-radius: .5rem;
@media only screen and (max-width: 450px) {
margin-top: 1rem;

View File

@@ -7,18 +7,18 @@ import { ActivatedRoute, Params, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import {
OidcIdpConfigCreate as AdminOidcIdpConfigCreate,
OIDCMappingField as authMappingFields,
OidcIdpConfigCreate as AdminOidcIdpConfigCreate,
OIDCMappingField as authMappingFields,
} from 'src/app/proto/generated/admin_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
import {
OidcIdpConfigCreate as MgmtOidcIdpConfigCreate,
OIDCMappingField as mgmtMappingFields,
OidcIdpConfigCreate as MgmtOidcIdpConfigCreate,
OIDCMappingField as mgmtMappingFields,
} from '../../proto/generated/management_pb';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
@Component({
selector: 'app-idp-create',
@@ -37,7 +37,7 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
public formGroup!: FormGroup;
public createSteps: number = 1;
public currentCreateStep: number = 1;
public loading: boolean = false;
constructor(
private router: Router,
private route: ActivatedRoute,
@@ -47,7 +47,6 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
) {
this.formGroup = new FormGroup({
name: new FormControl('', [Validators.required]),
logoSrc: new FormControl('', []),
clientId: new FormControl('', [Validators.required]),
clientSecret: new FormControl('', [Validators.required]),
issuer: new FormControl('', [Validators.required]),
@@ -68,8 +67,8 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
this.mappingFields = [
authMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
authMappingFields.OIDCMAPPINGFIELD_EMAIL];
authMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
authMappingFields.OIDCMAPPINGFIELD_EMAIL];
break;
}
});
@@ -91,25 +90,30 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
let req: AdminOidcIdpConfigCreate | MgmtOidcIdpConfigCreate;
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
req = new MgmtOidcIdpConfigCreate();
break;
case PolicyComponentServiceType.ADMIN:
req = new AdminOidcIdpConfigCreate();
break;
case PolicyComponentServiceType.MGMT:
req = new MgmtOidcIdpConfigCreate();
break;
case PolicyComponentServiceType.ADMIN:
req = new AdminOidcIdpConfigCreate();
break;
}
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value);
req.setIssuer(this.issuer?.value);
req.setLogoSrc(this.logoSrc?.value);
req.setScopesList(this.scopesList?.value);
req.setIdpDisplayNameMapping(this.idpDisplayNameMapping?.value);
req.setUsernameMapping(this.usernameMapping?.value);
this.loading = true;
this.service.CreateOidcIdp(req).then((idp) => {
this.router.navigate(['idp', idp.getId()]);
setTimeout(() => {
this.loading = false;
this.router.navigate([
this.serviceType === PolicyComponentServiceType.MGMT ? 'org' :
this.serviceType === PolicyComponentServiceType.ADMIN ? 'iam' : '',
'idp', idp.getId()]);
}, 2000);
}).catch(error => {
this.toast.showError(error);
});
@@ -147,10 +151,6 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
return this.formGroup.get('name');
}
public get logoSrc(): AbstractControl | null {
return this.formGroup.get('logoSrc');
}
public get clientId(): AbstractControl | null {
return this.formGroup.get('clientId');
}
@@ -163,7 +163,7 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
return this.formGroup.get('issuer');
}
public get scopesList(): AbstractControl | null {
return this.formGroup.get('scopesList');
return this.formGroup.get('scopesList');
}
public get idpDisplayNameMapping(): AbstractControl | null {
@@ -171,7 +171,7 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
}
public get usernameMapping(): AbstractControl | null {
return this.formGroup.get('usernameMapping');
return this.formGroup.get('usernameMapping');
}
}

View File

@@ -6,12 +6,13 @@ import { MatChipsModule } from '@angular/material/chips';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { IdpCreateRoutingModule } from './idp-create-routing.module';
import { IdpCreateComponent } from './idp-create.component';
import {MatSelectModule} from '@angular/material/select';
@NgModule({
declarations: [IdpCreateComponent],
@@ -28,6 +29,7 @@ import {MatSelectModule} from '@angular/material/select';
MatChipsModule,
MatTooltipModule,
TranslateModule,
MatProgressBarModule,
],
})
export class IdpCreateModule { }

View File

@@ -1,19 +1,19 @@
<app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length"
[timestamp]="idpResult?.viewTimestamp" [selection]="selection">
[emitRefreshOnPreviousRoutes]="['/iam/idp/create']" [timestamp]="idpResult?.viewTimestamp" [selection]="selection">
<ng-template appHasRole [appHasRole]="['iam.write']" actions>
<button (click)="deactivateSelectedIdps()" matTooltip="{{'IDP.DEACTIVATE' | translate}}" class="icon-button"
mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<mat-icon svgIcon="mdi_account_cancel"></mat-icon>
<mat-icon>block</mat-icon>
</button>
<button (click)="reactivateSelectedIdps()" matTooltip="{{'IDP.ACTIVATE' | translate}}" class="icon-button"
mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<mat-icon svgIcon="mdi_account_check_outline"></mat-icon>
<mat-icon>play_circle_outline</mat-icon>
</button>
<button color="warn" (click)="removeSelectedIdps()" matTooltip="{{'IDP.DELETE' | translate}}"
class="icon-button" mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<i class="las la-trash"></i>
</button>
<a class="add-button" [routerLink]="createRouterLink" color="primary" mat-raised-button [disabled]="disabled">
<a [routerLink]="createRouterLink" color="primary" mat-raised-button [disabled]="disabled">
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
</ng-template>
@@ -24,14 +24,14 @@
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
[indeterminate]="selection.hasValue() && !isAllSelected()"
[disabled]="serviceType==PolicyComponentServiceType.MGMT">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let idp">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
[disabled]="serviceType==PolicyComponentServiceType.MGMT && idp?.providerType == IdpProviderType.IDPPROVIDERTYPE_SYSTEM"
(change)="$event ? selection.toggle(idp) : null" [checked]="selection.isSelected(idp)">
<img *ngIf="idp?.logoSrc?.startsWith('https://'); else genAvatar" [src]="idp.logoSrc"
alt="ipp logo {{idp?.name}}" />
<ng-template #genAvatar>
<div class="avatar">
<span>{{idp.name.charAt(0)}}</span>
@@ -43,12 +43,12 @@
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.NAME' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{idp?.name}} </td>
<td [routerLink]="routerLinkForRow(idp)" mat-cell *matCellDef="let idp"> {{idp?.name}} </td>
</ng-container>
<ng-container matColumnDef="config">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.CONFIG' | translate }} </th>
<td mat-cell *matCellDef="let idp">
<td [routerLink]="routerLinkForRow(idp)" mat-cell *matCellDef="let idp">
<div *ngFor="let elem of idp?.oidcConfig | keyvalue" class="flex-row">
<span class="key">{{elem.key}}:</span>
<span class="value">{{elem.value}}</span>
@@ -58,28 +58,47 @@
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.STATE' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{ 'IDP.STATES.'+idp.state | translate }} </td>
<td [routerLink]="routerLinkForRow(idp)" mat-cell *matCellDef="let idp">
{{ 'IDP.STATES.'+idp.state | translate }} </td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.CREATIONDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let idp">
<td [routerLink]="routerLinkForRow(idp)" class="pointer" mat-cell *matCellDef="let idp">
{{idp.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.CHANGEDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let idp">
<td [routerLink]="routerLinkForRow(idp)" class="pointer" mat-cell *matCellDef="let idp">
{{idp.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;"
[routerLink]="row.id ? [serviceType == PolicyComponentServiceType.ADMIN ? '/iam' : '/org', 'idp', row.id ]: null">
</tr>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.TYPE' | translate }} </th>
<td [routerLink]="routerLinkForRow(idp)" class="pointer" mat-cell *matCellDef="let idp">
{{'IDP.TYPES.'+idp.providerType | translate }} </td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let idp">
<button
[disabled]="serviceType==PolicyComponentServiceType.MGMT && idp?.providerType == IdpProviderType.IDPPROVIDERTYPE_SYSTEM"
mat-icon-button color="warn" matTooltip="{{'IAM.VIEWS.CLEAR' | translate}}"
(click)="removeIdp(idp)">
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight"
[ngClass]="{'disabled': serviceType==PolicyComponentServiceType.MGMT && row?.providerType == IdpProviderType.IDPPROVIDERTYPE_SYSTEM}"
mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
<mat-paginator #paginator class="paginator" [length]="idpResult?.totalResult || 0" [pageSize]="10"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator>
</div>
<mat-paginator #paginator class="paginator" [length]="idpResult?.totalResult || 0" [pageSize]="10"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator>
</app-refresh-table>

View File

@@ -1,11 +1,10 @@
.table-wrapper {
overflow: auto;
width: 100%;
.table,
.paginator {
width: 100%;
td,
th {
padding: 0 1rem;
@@ -19,23 +18,29 @@
padding-right: 0;
}
}
.data-row {
cursor: pointer;
&:hover {
background-color: #ffffff05;
}
}
}
}
td {
outline: none;
}
tr {
outline: none;
}
.add-button {
border-radius: .5rem;
&.disabled * {
opacity: .8;
}
button {
visibility: hidden;
}
&:hover {
button {
visibility: visible;
}
}
}
.avatar {
@@ -57,14 +62,22 @@ tr {
.flex-row {
display: flex;
justify-content: space-between;
padding: 2px 0;
width: 250px;
.key {
margin-right: 1rem;
font-size: 14px;
margin-right: .5rem;
font-size: 12px;
flex: 1 0 50%;
overflow: hidden;
text-overflow: ellipsis;
}
.value {
font-size: 14px;
font-size: 12px;
flex: 1 0 50%;
overflow: hidden;
text-overflow: ellipsis;
}
&:first-child {

View File

@@ -1,17 +1,19 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { RouterLink } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { IdpSearchResponse as AdminIdpSearchResponse, IdpView as AdminIdpView } from 'src/app/proto/generated/admin_pb';
import { IdpView as MgmtIdpView } from 'src/app/proto/generated/management_pb';
import { IdpProviderType, IdpView as MgmtIdpView } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
@Component({
selector: 'app-idp-table',
@@ -31,12 +33,13 @@ export class IdpTableComponent implements OnInit {
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public IdpProviderType: any = IdpProviderType;
@Input() public displayedColumns: string[] = ['select', 'name', 'config', 'creationDate', 'changeDate', 'state'];
@Output() public changedSelection: EventEmitter<Array<AdminIdpView.AsObject | MgmtIdpView.AsObject>>
= new EventEmitter();
constructor(public translate: TranslateService, private toast: ToastService) {
constructor(public translate: TranslateService, private toast: ToastService, private dialog: MatDialog) {
this.selection.changed.subscribe(() => {
this.changedSelection.emit(this.selection.selected);
});
@@ -44,6 +47,13 @@ export class IdpTableComponent implements OnInit {
ngOnInit(): void {
this.getData(10, 0);
if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.displayedColumns = ['select', 'name', 'config', 'creationDate', 'changeDate', 'state', 'type'];
}
if (!this.disabled) {
this.displayedColumns.push('actions');
}
}
public isAllSelected(): boolean {
@@ -64,47 +74,79 @@ export class IdpTableComponent implements OnInit {
}
public deactivateSelectedIdps(): void {
this.selection.clear();
Promise.all(this.selection.selected.map(value => {
return this.service.DeactivateIdpConfig(value.id);
})).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
this.getData(10, 0);
this.toast.showInfo('IDP.TOAST.SELECTEDDEACTIVATED', true);
this.refreshPage();
});
}
public reactivateSelectedIdps(): void {
this.selection.clear();
Promise.all(this.selection.selected.map(value => {
return this.service.ReactivateIdpConfig(value.id);
})).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
this.getData(10, 0);
this.toast.showInfo('IDP.TOAST.SELECTEDREACTIVATED', true);
this.refreshPage();
});
}
public removeSelectedIdps(): void {
Promise.all(this.selection.selected.map(value => {
return this.service.RemoveIdpConfig(value.id);
})).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
this.getData(10, 0);
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'IDP.DELETE_SELECTION_TITLE',
descriptionKey: 'IDP.DELETE_SELECTION_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.selection.clear();
Promise.all(this.selection.selected.map(value => {
return this.service.RemoveIdpConfig(value.id);
})).then(() => {
this.toast.showInfo('IDP.TOAST.SELECTEDDEACTIVATED', true);
this.refreshPage();
});
}
});
}
public removeIdp(idp: AdminIdpView.AsObject | MgmtIdpView.AsObject): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'IDP.DELETE_TITLE',
descriptionKey: 'IDP.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.service.RemoveIdpConfig(idp.id).then(() => {
this.toast.showInfo('IDP.TOAST.REMOVED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
});
}
});
}
private async getData(limit: number, offset: number): Promise<void> {
this.loadingSubject.next(true);
// let query: AdminIdpSearchQuery | MgmtIdpSearchQuery;
// if (this.service instanceof AdminService) {
// query = new AdminIdpSearchQuery();
// query.setKey(AdminIdpSearchKey.)
// } else if (this.service instanceof ManagementService) {
// return ['/org', 'idp', 'create'];
// }
this.service.SearchIdps(limit, offset).then(resp => {
this.idpResult = resp.toObject();
this.dataSource.data = this.idpResult.resultList;
console.log(this.idpResult.resultList);
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
@@ -123,4 +165,21 @@ export class IdpTableComponent implements OnInit {
return ['/org', 'idp', 'create'];
}
}
public routerLinkForRow(row: MgmtIdpView.AsObject | AdminIdpView.AsObject): any {
if (row.id) {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
switch ((row as MgmtIdpView.AsObject).providerType) {
case IdpProviderType.IDPPROVIDERTYPE_SYSTEM:
return ['/iam', 'idp', row.id];
case IdpProviderType.IDPPROVIDERTYPE_ORG:
return ['/org', 'idp', row.id];
}
break;
case PolicyComponentServiceType.ADMIN:
return ['/iam', 'idp', row.id];
}
}
}
}

View File

@@ -11,8 +11,9 @@ import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { TruncatePipeModule } from 'src/app/pipes/truncate-pipe/truncate-pipe.module';
import { IdpTableComponent } from './idp-table.component';
@@ -34,6 +35,7 @@ import { IdpTableComponent } from './idp-table.component';
RouterModule,
RefreshTableModule,
HasRoleModule,
TruncatePipeModule,
],
exports: [
IdpTableComponent,

View File

@@ -1,91 +1,99 @@
<app-detail-layout [backRouterLink]="backroutes" [title]="'IDP.DETAIL.TITLE' | translate"
[description]="'IDP.DETAIL.DESCRIPTION' | translate">
<div class="container">
<form (ngSubmit)="updateIdp()">
<ng-container [formGroup]="idpForm">
<div class="content">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.ID' | translate }}</mat-label>
<input matInput formControlName="id" />
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.NAME' | translate }}</mat-label>
<input matInput formControlName="name" />
</mat-form-field>
<!--<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.LOGOSRC' | translate }}</mat-label>
<input matInput formControlName="logoSrc" />
</mat-form-field>-->
[description]="'IDP.DETAIL.DESCRIPTION' | translate">
<div class="container">
<form (ngSubmit)="updateIdp()">
<ng-container [formGroup]="idpForm">
<div class="content">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.ID' | translate }}</mat-label>
<input matInput formControlName="id" />
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.NAME' | translate }}</mat-label>
<input matInput formControlName="name" />
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.STYLE' | translate }}</mat-label>
<mat-select formControlName="stylingType">
<mat-option *ngFor="let field of styleFields" [value]="field">
{{ 'IDP.STYLEFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-container>
<div class="btn-wrapper">
<button color="primary" mat-raised-button class="continue-button" [disabled]="idpForm.invalid"
type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
<ng-container *ngIf="oidcConfigForm">
<h2>{{'IDP.DETAIL.OIDC.TITLE' | translate}}</h2>
<p>{{'IDP.DETAIL.OIDC.DESCRIPTION' | translate}}</p>
<form (ngSubmit)="updateOidcConfig()">
<ng-container [formGroup]="oidcConfigForm">
<div class="content">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.ISSUER' | translate }}</mat-label>
<input matInput formControlName="issuer" />
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.CLIENTID' | translate }}</mat-label>
<input matInput formControlName="clientId" />
</mat-form-field>
<mat-checkbox class="desc" [(ngModel)]="showIdSecretSection"
[ngModelOptions]="{standalone: true}">
Update Client Secret
</mat-checkbox>
<mat-form-field appearance="outline" class="formfield" *ngIf="showIdSecretSection">
<mat-label>{{ 'IDP.CLIENTSECRET' | translate }}</mat-label>
<input matInput formControlName="clientSecret" />
</mat-form-field>
<mat-form-field appearance="outline" class="formfield fullwidth">
<mat-label>{{ 'IDP.SCOPESLIST' | translate }}</mat-label>
<mat-chip-list #chipScopesList aria-label="scope selection">
<mat-chip class="chip" *ngFor="let scope of scopesList?.value" selectable="false"
removable (removed)="removeScope(scope)">
{{scope}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input [matChipInputFor]="chipScopesList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addScope($event)">
</mat-chip-list>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</mat-label>
<mat-select formControlName="idpDisplayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</mat-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-container>
<div class="btn-wrapper">
<button color="primary" mat-raised-button class="continue-button"
[disabled]="oidcConfigForm.invalid" type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</ng-container>
</div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="idpForm.invalid" type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</form>
<h2 *ngIf="oidcConfigForm">{{'IDP.DETAIL.OIDC.TITLE' | translate}}</h2>
<form (ngSubmit)="updateOidcConfig()" *ngIf="oidcConfigForm">
<ng-container [formGroup]="oidcConfigForm">
<div class="content">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.ISSUER' | translate }}</mat-label>
<input matInput formControlName="issuer" />
</mat-form-field>
</div>
<div class="content">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.CLIENTID' | translate }}</mat-label>
<input matInput formControlName="clientId" />
</mat-form-field>
</div>
<div class="content">
<mat-checkbox class="desc" [(ngModel)]="showIdSecretSection" [ngModelOptions]="{standalone: true}">
Update Client Secret
</mat-checkbox>
<mat-form-field appearance="outline" class="formfield" *ngIf="showIdSecretSection">
<mat-label>{{ 'IDP.CLIENTSECRET' | translate }}</mat-label>
<input matInput formControlName="clientSecret" />
</mat-form-field>
</div>
<div class="content">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.SCOPESLIST' | translate }}</mat-label>
<mat-chip-list #chipScopesList aria-label="scope selection" >
<mat-chip class="chip" *ngFor="let scope of scopesList?.value" selectable="false" removable
(removed)="removeScope(scope)">
{{scope}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input [matChipInputFor]="chipScopesList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true" (matChipInputTokenEnd)="addScope($event)">
</mat-chip-list>
</mat-form-field>
</div>
<div class="content">
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</mat-label>
<mat-select formControlName="idpDisplayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINTFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</mat-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINTFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-container>
<button color="primary" mat-raised-button class="continue-button" [disabled]="oidcConfigForm.invalid" type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</form>
</div>
</app-detail-layout>
</app-detail-layout>

View File

@@ -9,6 +9,7 @@
.content {
display: flex;
flex-direction: row;
margin: 0 -.5rem;
flex-wrap: wrap;
@@ -16,27 +17,35 @@
flex-basis: 100%;
margin: 0 .5rem;
margin-bottom: 1rem;
color: #8795a1;
color: var(--grey);
}
.formfield {
flex: 1 0 auto;
flex: 1 1 auto;
margin: 0 .5rem;
&.fullwidth {
flex-basis: 100%;
}
@media only screen and (max-width: 450px) {
flex-basis: 100%;
}
}
}
.continue-button {
margin-bottom: 4rem;
display: block;
padding: .5rem 4rem;
border-radius: .5rem;
.btn-wrapper {
display: flex;
justify-content: flex-end;
@media only screen and (max-width: 450px) {
margin-top: 1rem;
margin-bottom: 2rem;
.continue-button {
margin-bottom: 4rem;
display: block;
padding: .5rem 4rem;
@media only screen and (max-width: 450px) {
margin-top: 1rem;
margin-bottom: 2rem;
}
}
}

View File

@@ -1,20 +1,22 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common';
import {Component, Injector, Input, OnDestroy, OnInit, Type} from '@angular/core';
import { Component, Injector, OnDestroy, OnInit, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute, Params } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap, take } from 'rxjs/operators';
import {
OIDCMappingField as authMappingFields,
OidcIdpConfigUpdate as AdminOidcIdpConfigUpdate,
IdpUpdate as AdminIdpConfigUpdate,
IdpStylingType as adminIdpStylingType,
IdpUpdate as AdminIdpConfigUpdate,
OidcIdpConfigUpdate as AdminOidcIdpConfigUpdate,
OIDCMappingField as adminMappingFields,
} from 'src/app/proto/generated/admin_pb';
import {
OIDCMappingField as mgmtMappingFields,
OidcIdpConfigUpdate as MgmtOidcIdpConfigUpdate,
IdpUpdate as MgmtIdpConfigUpdate,
IdpStylingType as mgmtIdpStylingType,
IdpUpdate as MgmtIdpConfigUpdate,
OidcIdpConfigUpdate as MgmtOidcIdpConfigUpdate,
OIDCMappingField as mgmtMappingFields,
} from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
@@ -28,7 +30,9 @@ import { PolicyComponentServiceType } from '../policies/policy-component-types.e
styleUrls: ['./idp.component.scss'],
})
export class IdpComponent implements OnInit, OnDestroy {
public mappingFields: mgmtMappingFields[] | authMappingFields[] = [];
public mappingFields: mgmtMappingFields[] | adminMappingFields[] = [];
public styleFields: mgmtIdpStylingType[] | adminIdpStylingType[] = [];
public showIdSecretSection: boolean = false;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
private service!: ManagementService | AdminService;
@@ -41,7 +45,6 @@ export class IdpComponent implements OnInit, OnDestroy {
public oidcConfigForm!: FormGroup;
constructor(
// private router: Router,
private toast: ToastService,
private injector: Injector,
private route: ActivatedRoute,
@@ -50,33 +53,38 @@ export class IdpComponent implements OnInit, OnDestroy {
this.idpForm = new FormGroup({
id: new FormControl({ disabled: true, value: '' }, [Validators.required]),
name: new FormControl('', [Validators.required]),
logoSrc: new FormControl({ disabled: true, value: '' }, [Validators.required]),
stylingType: new FormControl('', [Validators.required]),
});
this.oidcConfigForm = new FormGroup({
clientId: new FormControl('', [Validators.required]),
clientSecret: new FormControl(''),
issuer: new FormControl('', [Validators.required]),
scopesList: new FormControl([], []),
idpDisplayNameMapping: new FormControl(0),
usernameMapping: new FormControl(0),
clientId: new FormControl('', [Validators.required]),
clientSecret: new FormControl(''),
issuer: new FormControl('', [Validators.required]),
scopesList: new FormControl([], []),
idpDisplayNameMapping: new FormControl(0),
usernameMapping: new FormControl(0),
});
this.route.data.pipe(switchMap(data => {
console.log(data.serviceType);
this.serviceType = data.serviceType;
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.mappingFields = [
mgmtMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
mgmtMappingFields.OIDCMAPPINGFIELD_EMAIL];
mgmtMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
mgmtMappingFields.OIDCMAPPINGFIELD_EMAIL];
this.styleFields = [
mgmtIdpStylingType.IDPSTYLINGTYPE_UNSPECIFIED,
mgmtIdpStylingType.IDPSTYLINGTYPE_GOOGLE];
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
this.mappingFields = [
authMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
authMappingFields.OIDCMAPPINGFIELD_EMAIL];
adminMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
adminMappingFields.OIDCMAPPINGFIELD_EMAIL];
this.styleFields = [
adminIdpStylingType.IDPSTYLINGTYPE_UNSPECIFIED,
adminIdpStylingType.IDPSTYLINGTYPE_GOOGLE];
break;
}
@@ -85,11 +93,11 @@ export class IdpComponent implements OnInit, OnDestroy {
const { id } = params;
if (id) {
this.service.IdpByID(id).then(idp => {
const idpObject = idp.toObject();
this.idpForm.patchValue(idpObject);
if (idpObject.oidcConfig) {
this.oidcConfigForm.patchValue(idpObject.oidcConfig);
}
const idpObject = idp.toObject();
this.idpForm.patchValue(idpObject);
if (idpObject.oidcConfig) {
this.oidcConfigForm.patchValue(idpObject.oidcConfig);
}
});
}
});
@@ -121,10 +129,10 @@ export class IdpComponent implements OnInit, OnDestroy {
req.setId(this.id?.value);
req.setName(this.name?.value);
req.setLogoSrc(this.logoSrc?.value);
req.setStylingType(this.stylingType?.value);
this.service.UpdateIdp(req).then((idp) => {
this.toast.showInfo('IDP.TOAST.SAVED', true);
this.toast.showInfo('IDP.TOAST.SAVED', true);
// this.router.navigate(['idp', ]);
}).catch(error => {
this.toast.showError(error);
@@ -132,31 +140,31 @@ export class IdpComponent implements OnInit, OnDestroy {
}
public updateOidcConfig(): void {
let req: AdminOidcIdpConfigUpdate | MgmtOidcIdpConfigUpdate;
let req: AdminOidcIdpConfigUpdate | MgmtOidcIdpConfigUpdate;
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
req = new MgmtOidcIdpConfigUpdate();
break;
case PolicyComponentServiceType.ADMIN:
req = new AdminOidcIdpConfigUpdate();
break;
}
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
req = new MgmtOidcIdpConfigUpdate();
break;
case PolicyComponentServiceType.ADMIN:
req = new AdminOidcIdpConfigUpdate();
break;
}
req.setIdpId(this.id?.value);
req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value);
req.setIssuer(this.issuer?.value);
req.setScopesList(this.scopesList?.value);
req.setUsernameMapping(this.usernameMapping?.value);
req.setIdpDisplayNameMapping(this.idpDisplayNameMapping?.value);
req.setIdpId(this.id?.value);
req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value);
req.setIssuer(this.issuer?.value);
req.setScopesList(this.scopesList?.value);
req.setUsernameMapping(this.usernameMapping?.value);
req.setIdpDisplayNameMapping(this.idpDisplayNameMapping?.value);
this.service.UpdateOidcIdpConfig(req).then((oidcConfig) => {
this.toast.showInfo('IDP.TOAST.SAVED', true);
// this.router.navigate(['idp', ]);
}).catch(error => {
this.toast.showError(error);
});
this.service.UpdateOidcIdpConfig(req).then((oidcConfig) => {
this.toast.showInfo('IDP.TOAST.SAVED', true);
// this.router.navigate(['idp', ]);
}).catch(error => {
this.toast.showError(error);
});
}
public close(): void {
@@ -190,12 +198,10 @@ export class IdpComponent implements OnInit, OnDestroy {
public get backroutes(): string[] {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return ['/org', 'policy', 'login'];
return ['/org', 'policy', 'login'];
case PolicyComponentServiceType.ADMIN:
return ['/iam', 'policy', 'login'];
break;
return ['/iam', 'policy', 'login'];
}
return [];
}
public get id(): AbstractControl | null {
@@ -206,8 +212,8 @@ export class IdpComponent implements OnInit, OnDestroy {
return this.idpForm.get('name');
}
public get logoSrc(): AbstractControl | null {
return this.idpForm.get('logoSrc');
public get stylingType(): AbstractControl | null {
return this.idpForm.get('stylingType');
}
public get clientId(): AbstractControl | null {
@@ -227,10 +233,10 @@ export class IdpComponent implements OnInit, OnDestroy {
}
public get idpDisplayNameMapping(): AbstractControl | null {
return this.oidcConfigForm.get('idpDisplayNameMapping');
return this.oidcConfigForm.get('idpDisplayNameMapping');
}
public get usernameMapping(): AbstractControl | null {
return this.oidcConfigForm.get('usernameMapping');
return this.oidcConfigForm.get('usernameMapping');
}
}

View File

@@ -0,0 +1,98 @@
<app-refresh-table *ngIf="dataSource" (refreshed)="changePage()" [dataSize]="dataSource.totalResult"
[timestamp]="dataSource.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async">
<ng-container actions *ngIf="selection.hasValue()">
<ng-content select="[selectactions]"></ng-content>
</ng-container>
<div actions>
<ng-content select="[writeactions]"></ng-content>
</div>
<div class="table-wrapper">
<table mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="!canWrite" color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox [disabled]="!canWrite" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<app-avatar *ngIf="row?.displayName && row.firstName && row.lastName; else cog" class="avatar"
[name]="row.displayName" [size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon">
<i class="las la-user-cog"></i>
</div>
</ng-template>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="userId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERID' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.userId}} </td>
</ng-container>
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.FIRSTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LASTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.lastName}} </td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.EMAIL' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.email}}
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let view">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn"
(click)="triggerDeleteMember(view)" mat-icon-button><i class="las la-trash"></i></button>
</td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'ROLESLABEL' | translate }} </th>
<td mat-cell *matCellDef="let member">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'ROLESLABEL' | translate }}</mat-label>
<mat-select [(ngModel)]="member.rolesList" multiple [disabled]="!canWrite"
(selectionChange)="updateRoles.emit({member: member, change: $event})">
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
{{ role }}
</mat-option>
</mat-select>
</mat-form-field>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
</div>
<mat-paginator *ngIf="dataSource" class="paginator" #paginator [pageSize]="INITIALPAGESIZE"
[length]="dataSource.totalResult" [pageSizeOptions]="[25, 50, 100, 250]" (page)="changePage($event)">
</mat-paginator>
</app-refresh-table>

View File

@@ -1,9 +1,14 @@
.icon-button {
margin-right: .5rem;
}
.table-wrapper {
overflow: auto;
overflow-x: auto;
.table,
.paginator {
width: 100%;
td,
th {
padding: .5rem;
@@ -22,20 +27,31 @@
width: 40px;
}
.data-row {
&:hover {
background-color: #ffffff05;
}
}
.selection {
width: 50px;
max-width: 50px;
}
}
.role {
display: inline-block;
margin: .25rem;
tr {
button {
visibility: hidden;
}
&:hover {
button {
visibility: visible;
}
}
}
.sa-icon {
display: block;
width: 32px;
margin: 0 .5rem;
i {
margin: auto;
}
}
}

View File

@@ -4,15 +4,15 @@ import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ProjectGrantMembersComponent } from './project-grant-members.component';
import { MembersTableComponent } from './members-table.component';
describe('ProjectMembersComponent', () => {
let component: ProjectGrantMembersComponent;
let fixture: ComponentFixture<ProjectGrantMembersComponent>;
describe('MembersTableComponent', () => {
let component: MembersTableComponent;
let fixture: ComponentFixture<MembersTableComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectGrantMembersComponent],
declarations: [MembersTableComponent],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
@@ -23,7 +23,7 @@ describe('ProjectMembersComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectGrantMembersComponent);
fixture = TestBed.createComponent(MembersTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -0,0 +1,83 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table';
import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { IamMembersDataSource } from 'src/app/pages/iam/iam-members/iam-members-datasource';
import { OrgMembersDataSource } from 'src/app/pages/orgs/org-members/org-members-datasource';
import { IamMemberView } from 'src/app/proto/generated/admin_pb';
import { OrgMemberView, ProjectMemberView } from 'src/app/proto/generated/management_pb';
import { ProjectMembersDataSource } from '../project-members/project-members-datasource';
type View = OrgMemberView.AsObject | ProjectMemberView.AsObject | IamMemberView.AsObject;
type MemberDatasource = OrgMembersDataSource | ProjectMembersDataSource | IamMembersDataSource;
@Component({
selector: 'app-members-table',
templateUrl: './members-table.component.html',
styleUrls: ['./members-table.component.scss'],
})
export class MembersTableComponent implements OnInit, OnDestroy {
public INITIALPAGESIZE: number = 25;
@Input() public canDelete: boolean = false;
@Input() public canWrite: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild(MatTable) public table!: MatTable<View>;
@Input() public dataSource!: MemberDatasource;
public selection: SelectionModel<any> = new SelectionModel<any>(true, []);
@Input() public memberRoleOptions: string[] = [];
@Input() public factoryLoadFunc!: Function;
@Input() public refreshTrigger!: Observable<void>;
@Output() public updateRoles: EventEmitter<{ member: View, change: MatSelectChange; }> = new EventEmitter();
@Output() public changedSelection: EventEmitter<any[]> = new EventEmitter();
@Output() public deleteMember: EventEmitter<View> = new EventEmitter();
private destroyed: Subject<void> = new Subject();
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'userId', 'firstname', 'lastname', 'username', 'email', 'roles'];
constructor() {
this.selection.changed.pipe(takeUntil(this.destroyed)).subscribe(_ => {
this.changedSelection.emit(this.selection.selected);
});
}
public ngOnInit(): void {
this.refreshTrigger.pipe(takeUntil(this.destroyed)).subscribe(() => {
this.changePage(this.paginator);
});
if (this.canDelete) {
this.displayedColumns.push('actions');
}
}
public ngOnDestroy(): void {
this.destroyed.next();
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.membersSubject.value.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.membersSubject.value.forEach(row => this.selection.select(row));
}
public changePage(event?: PageEvent | MatPaginator): any {
this.selection.clear();
return this.factoryLoadFunc(event ?? this.paginator);
}
public triggerDeleteMember(member: any): void {
this.deleteMember.emit(member);
}
}

View File

@@ -0,0 +1,45 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { AvatarModule } from '../avatar/avatar.module';
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
import { MembersTableComponent } from './members-table.component';
@NgModule({
declarations: [
MembersTableComponent,
],
imports: [
CommonModule,
MatFormFieldModule,
MatSelectModule,
MatCheckboxModule,
MatIconModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatTooltipModule,
FormsModule,
TranslateModule,
RefreshTableModule,
RouterModule,
AvatarModule,
MatButtonModule,
],
exports: [
MembersTableComponent,
],
})
export class MembersTableModule { }

View File

@@ -2,7 +2,7 @@
display: flex;
height: 100%;
overflow-x: hidden;
transition: all .2s ease-in-out;
transition: all .3s cubic-bezier(.645, .045, .355, 1);
.main-content {
display: relative;

View File

@@ -1,6 +1,7 @@
.validation-col {
display: flex wrap;
display: flex;
flex-wrap: wrap;
padding: 1rem 0;
width: 100%;
@@ -16,7 +17,7 @@
span {
font-size: 14px;
color: #8795a1;
color: var(--grey);
}
.sp-wrapper {

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LabelPolicyComponent } from './label-policy.component';
const routes: Routes = [
{
path: '',
component: LabelPolicyComponent,
data: {
animation: 'DetailPage',
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class LabelPolicyRoutingModule { }

View File

@@ -0,0 +1,20 @@
<app-detail-layout [backRouterLink]="['/iam']" [title]="'POLICY.LABEL.TITLE' | translate"
[description]="'POLICY.LABEL.DESCRIPTION' | translate">
<div class="content" *ngIf="labelData">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{'POLICY.LABEL.PRIMARYCOLOR' | translate}}</mat-label>
<input [(ngModel)]="labelData.primaryColor" matInput />
</mat-form-field>
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{'POLICY.LABEL.SECONDARYCOLOR' | translate}}</mat-label>
<input [(ngModel)]="labelData.secondaryColor" matInput />
</mat-form-field>
</div>
<div class="btn-container">
<button (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</app-detail-layout>

View File

@@ -0,0 +1,24 @@
.content {
padding-top: 1rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
.form-field {
flex: 1;
margin: 0 .5rem;
}
}
.btn-container {
display: flex;
justify-content: flex-end;
width: 100%;
button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
}
}

View File

@@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IamPolicyGridComponent } from './iam-policy-grid.component';
import { LabelPolicyComponent } from './label-policy.component';
describe('IamPolicyGridComponent', () => {
let component: IamPolicyGridComponent;
let fixture: ComponentFixture<IamPolicyGridComponent>;
describe('LabelPolicyComponent', () => {
let component: LabelPolicyComponent;
let fixture: ComponentFixture<LabelPolicyComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [IamPolicyGridComponent],
declarations: [LabelPolicyComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IamPolicyGridComponent);
fixture = TestBed.createComponent(LabelPolicyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -0,0 +1,54 @@
import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { DefaultLabelPolicyUpdate, DefaultLabelPolicyView } from 'src/app/proto/generated/admin_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({
selector: 'app-label-policy',
templateUrl: './label-policy.component.html',
styleUrls: ['./label-policy.component.scss'],
})
export class LabelPolicyComponent implements OnDestroy {
public labelData!: DefaultLabelPolicyView.AsObject;
private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(
private route: ActivatedRoute,
private toast: ToastService,
private adminService: AdminService,
) {
this.route.params.subscribe(() => {
this.getData().then(data => {
if (data) {
this.labelData = data.toObject();
}
});
});
}
public ngOnDestroy(): void {
this.sub.unsubscribe();
}
private async getData(): Promise<DefaultLabelPolicyView> {
return this.adminService.GetDefaultLabelPolicy();
}
public savePolicy(): void {
const req = new DefaultLabelPolicyUpdate();
req.setPrimaryColor(this.labelData.primaryColor);
req.setSecondaryColor(this.labelData.secondaryColor);
this.adminService.UpdateDefaultLabelPolicy(req).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
}
}

View File

@@ -11,13 +11,13 @@ import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { PasswordIamPolicyRoutingModule } from './password-iam-policy-routing.module';
import { PasswordIamPolicyComponent } from './password-iam-policy.component';
import { LabelPolicyRoutingModule } from './label-policy-routing.module';
import { LabelPolicyComponent } from './label-policy.component';
@NgModule({
declarations: [PasswordIamPolicyComponent],
declarations: [LabelPolicyComponent],
imports: [
PasswordIamPolicyRoutingModule,
LabelPolicyRoutingModule,
CommonModule,
FormsModule,
MatInputModule,
@@ -31,4 +31,4 @@ import { PasswordIamPolicyComponent } from './password-iam-policy.component';
DetailLayoutModule,
],
})
export class PasswordIamPolicyModule { }
export class LabelPolicyModule { }

View File

@@ -6,7 +6,7 @@
<div mat-dialog-content>
<mat-form-field *ngIf="serviceType == PolicyComponentServiceType.MGMT" class="full-width" appearance="outline">
<mat-label>{{ 'IDP.TYPE' | translate }}</mat-label>
<mat-select [(ngModel)]="idpType" (selectionChange)="loadIdps()" (selectionChange)="loadIdps()">
<mat-select [(ngModel)]="idpType" (selectionChange)="loadIdps()">
<mat-option *ngFor="let type of idpTypes" [value]="type">
{{ 'IDP.TYPES.'+type | translate}}
</mat-option>

View File

@@ -3,7 +3,7 @@
}
.desc {
color: #8795a1;
color: var(--grey);
font-size: .9rem;
}
@@ -18,8 +18,4 @@
.ok-button {
margin-left: .5rem;
}
button {
border-radius: .5rem;
}
}

View File

@@ -61,7 +61,9 @@ export class AddIdpDialogComponent {
query.setKey(IdpSearchKey.IDPSEARCHKEY_PROVIDER_TYPE);
query.setMethod(SearchMethod.SEARCHMETHOD_EQUALS);
query.setValue(this.idpType.toString());
this.mgmtService.SearchIdps().then(idps => {
console.log(this.idpType);
console.log(query.toObject());
this.mgmtService.SearchIdps(undefined, undefined, [query]).then(idps => {
this.availableIdps = idps.toObject().resultList;
});
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {

View File

@@ -1,60 +1,76 @@
<app-detail-layout [backRouterLink]="backroutes" [title]="'ORG.POLICY.LOGIN_POLICY.TITLECREATE' | translate"
[description]="'ORG.POLICY.LOGIN_POLICY.DESCRIPTIONCREATE' | translate">
<ng-container *ngIf="(['policy.delete'] | hasRole | async) && serviceType == PolicyComponentServiceType.MGMT">
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn" (click)="deletePolicy()"
mat-stroked-button>
{{'ORG.POLICY.DELETE' | translate}}
</button>
<app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
[title]="'POLICY.LOGIN_POLICY.TITLE' | translate"
[description]="(serviceType==PolicyComponentServiceType.MGMT ? 'POLICY.LOGIN_POLICY.DESCRIPTIONCREATEMGMT' : PolicyComponentServiceType.ADMIN ? 'POLICY.LOGIN_POLICY.DESCRIPTIONCREATEADMIN' : '') | translate">
<p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div>
<ng-container *ngIf="serviceType === PolicyComponentServiceType.MGMT">
<ng-template appHasRole [appHasRole]="['policy.delete']">
<button *ngIf="!isDefault" matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()"
mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['policy.write']">
<button *ngIf="isDefault" matTooltip="{{'POLICY.CREATECUSTOM' | translate}}" (click)="savePolicy()"
mat-raised-button>
{{'POLICY.CREATECUSTOM' | translate}}
</button>
</ng-template>
</ng-container>
<div class="content" *ngIf="loginData">
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl
[(ngModel)]="loginData.allowUsernamePassword">
{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate}}
</mat-slide-toggle>
<p>{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD_DESC' | translate}}</p>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.ALLOWREGISTER' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl [(ngModel)]="loginData.allowRegister">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl
[(ngModel)]="loginData.allowRegister">
{{'POLICY.DATA.ALLOWREGISTER' | translate}}
</mat-slide-toggle>
<p> {{'POLICY.DATA.ALLOWREGISTER_DESC' | translate}} </p>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.ALLOWEXTERNALIDP' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl
[(ngModel)]="loginData.allowExternalIdp">
{{'POLICY.DATA.ALLOWEXTERNALIDP' | translate}}
</mat-slide-toggle>
<p> {{'POLICY.DATA.ALLOWEXTERNALIDP_DESC' | translate}} </p>
</div>
</div>
<p class="subheader">{{'LOGINPOLICY.IDPS' | translate}}</p>
<h3 class="subheader">{{'LOGINPOLICY.IDPS' | translate}}</h3>
<div class="idps">
<div class="idp" *ngFor="let idp of idps">
<mat-icon (click)="removeIdp(idp)" class="rm">remove_circle</mat-icon>
<span>{{idp.name}}</span>
<div class="idp" [ngClass]="{'disabled': disabled}" *ngFor="let idp of idps">
<button [disabled]="disabled" mat-icon-button (click)="removeIdp(idp)" class="rm">
<mat-icon matTooltip="{{'ACTIONS.REMOVE' | translate}}">
remove_circle</mat-icon>
</button>
<span class="name">{{idp.name}}</span>
<span class="meta">{{ 'IDP.TYPE' | translate }}: {{ 'IDP.TYPES.'+idp.type | translate }}</span>
<span class="meta">{{ 'IDP.ID' | translate }}: {{idp.idpConfigId}}</span>
</div>
<div class="new-idp" (click)="openDialog()">
<div *ngIf="!disabled" class="new-idp" (click)="openDialog()">
<mat-icon>add</mat-icon>
</div>
</div>
<div class="btn-container">
<button (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
<button [disabled]="disabled" class="save-button" (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
<ng-template appHasRole [appHasRole]="['org.idp.read']">
<app-card title="{{ 'IDP.LIST.TITLE' | translate }}" description="{{ 'IDP.LIST.DESCRIPTION' | translate }}">
<h2>{{ 'IDP.LIST.TITLE' | translate }}</h2>
<p>{{ 'IDP.LIST.DESCRIPTION' | translate }}</p>
<app-idp-table [service]="service" [serviceType]="serviceType"
[disabled]="(['iam.idp.write'] | hasRole | async) == false">
[disabled]="([serviceType == PolicyComponentServiceType.ADMIN ? 'iam.idp.write' : serviceType == PolicyComponentServiceType.MGMT ? 'org.idp.write' : ''] | hasRole | async) == false">
</app-idp-table>
</app-card>
</ng-template>
</app-detail-layout>
</app-detail-layout>

View File

@@ -1,46 +1,36 @@
.default {
color: #5282c1;
margin-top: 0;
}
button {
border-radius: .5rem;
.spinner-wr {
margin: .5rem 0;
}
.content {
padding-top: 1rem;
display: flex;
flex-direction: column;
width: 100%;
.row {
display: flex;
align-items: center;
padding: .5rem 0;
.left-desc {
color: #8795a1;
font-size: .9rem;
.toggle {
margin: .3rem 0;
}
.fill-space {
flex: 1;
}
.length-wrapper {
display: flex;
align-items: center;
p {
margin-top: .5rem;
font-size: 14px;
color: var(--grey);
}
}
}
.btn-container {
display: flex;
justify-content: flex-end;
width: 100%;
.save-button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
}
button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
border-radius: .5rem;
}
.idp-table-card {
width: 100%;
}
.subheader {
@@ -58,10 +48,16 @@ button {
justify-content: center;
margin: .5rem;
padding: 10px;
border: 1px solid #8795a1;
border: 1px solid var(--grey);
border-radius: .5rem;
cursor: pointer;
position: relative;
min-height: 70px;
min-width: 150px;
.name {
font-weight: 700;
}
span {
padding: 2px;
@@ -69,6 +65,7 @@ button {
.meta {
font-size: 12px;
color: var(--grey);
}
.rm {
@@ -77,10 +74,16 @@ button {
left: 0;
transform: translateX(-50%) translateY(-50%);
cursor: pointer;
&[disabled] {
display: none;
}
}
&:hover {
background-color: #ffffff10;
&:not(.disabled) {
&:hover {
background-color: #ffffff10;
}
}
img {

View File

@@ -1,7 +1,6 @@
import { Component, Injector, OnDestroy, Type } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { Empty } from 'google-protobuf/google/protobuf/empty_pb';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import {
@@ -11,11 +10,11 @@ import {
IdpView as AdminIdpView,
} from 'src/app/proto/generated/admin_pb';
import {
IdpProviderType,
IdpProviderView as MgmtIdpProviderView,
IdpView as MgmtIdpView,
LoginPolicy,
LoginPolicyView, OrgDomainView,
IdpProviderType,
IdpProviderView as MgmtIdpProviderView,
IdpView as MgmtIdpView,
LoginPolicy,
LoginPolicyView,
} from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
@@ -34,18 +33,19 @@ export class LoginPolicyComponent implements OnDestroy {
private sub: Subscription = new Subscription();
public service!: ManagementService | AdminService;
PolicyComponentServiceType: any = PolicyComponentServiceType;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public idps: MgmtIdpProviderView.AsObject[] | AdminIdpProviderView.AsObject[] = [];
public loading: boolean = false;
public disabled: boolean = true;
constructor(
private route: ActivatedRoute,
private router: Router,
private toast: ToastService,
private dialog: MatDialog,
private injector: Injector,
) {
this.sub = this.route.data.pipe(switchMap(data => {
console.log(data.serviceType);
this.serviceType = data.serviceType;
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
@@ -58,15 +58,20 @@ export class LoginPolicyComponent implements OnDestroy {
return this.route.params;
})).subscribe(() => {
this.getData().then(data => {
if (data) {
this.loginData = data.toObject();
}
});
this.getIdps().then(idps => {
console.log(idps);
this.idps = idps;
});
this.fetchData();
});
}
private fetchData(): void {
this.getData().then(data => {
if (data) {
this.loginData = data.toObject();
this.loading = false;
this.disabled = ((this.loginData as LoginPolicyView.AsObject)?.pb_default) ?? false;
}
});
this.getIdps().then(idps => {
this.idps = idps;
});
}
@@ -107,7 +112,11 @@ export class LoginPolicyComponent implements OnDestroy {
mgmtreq.setAllowExternalIdp(this.loginData.allowExternalIdp);
mgmtreq.setAllowRegister(this.loginData.allowRegister);
mgmtreq.setAllowUsernamePassword(this.loginData.allowUsernamePassword);
return (this.service as ManagementService).UpdateLoginPolicy(mgmtreq);
if ((this.loginData as LoginPolicyView.AsObject).pb_default) {
return (this.service as ManagementService).CreateLoginPolicy(mgmtreq);
} else {
return (this.service as ManagementService).UpdateLoginPolicy(mgmtreq);
}
case PolicyComponentServiceType.ADMIN:
const adminreq = new DefaultLoginPolicy();
adminreq.setAllowExternalIdp(this.loginData.allowExternalIdp);
@@ -119,25 +128,27 @@ export class LoginPolicyComponent implements OnDestroy {
public savePolicy(): void {
this.updateData().then(() => {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.router.navigate(['org']);
break;
case PolicyComponentServiceType.ADMIN:
this.router.navigate(['iam']);
break;
}
this.toast.showInfo('POLICY.LOGIN_POLICY.SAVED', true);
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
}).catch(error => {
this.toast.showError(error);
});
}
public deletePolicy(): Promise<Empty> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).RemoveLoginPolicy();
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).GetDefaultLoginPolicy();
public removePolicy(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
(this.service as ManagementService).RemoveLoginPolicy().then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
}).catch(error => {
this.toast.showError(error);
});
}
}
@@ -151,7 +162,14 @@ export class LoginPolicyComponent implements OnDestroy {
dialogRef.afterClosed().subscribe(resp => {
if (resp && resp.idp && resp.type) {
this.addIdp(resp.idp, resp.type);
this.addIdp(resp.idp, resp.type).then(() => {
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
}).catch(error => {
this.toast.showError(error);
});
}
});
}
@@ -169,23 +187,29 @@ export class LoginPolicyComponent implements OnDestroy {
public removeIdp(idp: AdminIdpProviderView.AsObject | MgmtIdpProviderView.AsObject): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
(this.service as ManagementService).RemoveIdpProviderFromLoginPolicy(idp.idpConfigId);
(this.service as ManagementService).RemoveIdpProviderFromLoginPolicy(idp.idpConfigId).then(() => {
const index = this.idps.findIndex(temp => temp === idp);
if (index > -1) {
this.idps.splice(index, 1);
}
});
break;
case PolicyComponentServiceType.ADMIN:
(this.service as AdminService).RemoveIdpProviderFromDefaultLoginPolicy(idp.idpConfigId);
(this.service as AdminService).RemoveIdpProviderFromDefaultLoginPolicy(idp.idpConfigId).then(() => {
const index = this.idps.findIndex(temp => temp === idp);
if (index > -1) {
this.idps.splice(index, 1);
}
});
break;
}
}
public get backroutes(): string[] {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return ['/org'];
case PolicyComponentServiceType.ADMIN:
return ['/iam'];
break;
public get isDefault(): boolean {
if (this.loginData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.loginData as LoginPolicyView.AsObject).pb_default;
} else {
return false;
}
}
return [];
}
}

View File

@@ -2,21 +2,22 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { CardModule } from 'src/app/modules/card/card.module';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
import { IdpTableModule } from 'src/app/modules/idp-table/idp-table.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { AddIdpDialogModule } from './add-idp-dialog/add-idp-dialog.module';
import { LoginPolicyRoutingModule } from './login-policy-routing.module';
import { LoginPolicyComponent } from './login-policy.component';
import { IdpTableModule } from 'src/app/modules/idp-table/idp-table.module';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
@NgModule({
declarations: [LoginPolicyComponent],
@@ -37,6 +38,7 @@ import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
DetailLayoutModule,
AddIdpDialogModule,
IdpTableModule,
MatProgressSpinnerModule,
],
})
export class LoginPolicyModule { }

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { OrgIamPolicyComponent } from './org-iam-policy.component';
const routes: Routes = [
{
path: '',
component: OrgIamPolicyComponent,
data: {
animation: 'DetailPage',
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrgIamPolicyRoutingModule { }

View File

@@ -0,0 +1,26 @@
<app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
[title]="'POLICY.IAM_POLICY.TITLECREATE' | translate" [description]="'POLICY.IAM_POLICY.DESCRIPTION' | translate">
<p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
<ng-template appHasRole [appHasRole]="['iam.policy.delete']">
<button *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()" mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button>
</ng-template>
<div class="content" *ngIf="iamData">
<div class="row">
<span class="left-desc">{{'POLICY.DATA.USERLOGINMUSTBEDOMAIN' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
[(ngModel)]="iamData.userLoginMustBeDomain">
</mat-slide-toggle>
</div>
</div>
<div class="btn-container">
<button (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</app-detail-layout>

View File

@@ -1,6 +1,6 @@
button {
border-radius: .5rem;
.default {
color: #5282c1;
margin-top: 0;
}
.content {
@@ -12,10 +12,9 @@ button {
.row {
display: flex;
align-items: center;
padding: .5rem 0;
padding: .3rem 0;
.left-desc {
color: #8795a1;
font-size: .9rem;
}
@@ -39,6 +38,5 @@ button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
border-radius: .5rem;
}
}

View File

@@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PasswordIamPolicyComponent } from './password-iam-policy.component';
import { OrgIamPolicyComponent } from './org-iam-policy.component';
describe('PasswordIamPolicyComponent', () => {
let component: PasswordIamPolicyComponent;
let fixture: ComponentFixture<PasswordIamPolicyComponent>;
describe('OrgIamPolicyComponent', () => {
let component: OrgIamPolicyComponent;
let fixture: ComponentFixture<OrgIamPolicyComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PasswordIamPolicyComponent],
declarations: [OrgIamPolicyComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PasswordIamPolicyComponent);
fixture = TestBed.createComponent(OrgIamPolicyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -0,0 +1,136 @@
import { Component, Injector, Input, OnDestroy, Type } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { OrgIamPolicyView as AdminOrgIamPolicyView } from 'src/app/proto/generated/admin_pb';
import { Org } from 'src/app/proto/generated/auth_pb';
import { OrgIamPolicyView as MgmtOrgIamPolicyView } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({
selector: 'app-org-iam-policy',
templateUrl: './org-iam-policy.component.html',
styleUrls: ['./org-iam-policy.component.scss'],
})
export class OrgIamPolicyComponent implements OnDestroy {
@Input() service!: AdminService;
private managementService!: ManagementService;
public serviceType!: PolicyComponentServiceType;
public iamData!: AdminOrgIamPolicyView.AsObject | MgmtOrgIamPolicyView.AsObject;
private sub: Subscription = new Subscription();
private org!: Org.AsObject;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(
private route: ActivatedRoute,
private toast: ToastService,
private sessionStorage: StorageService,
private injector: Injector,
private adminService: AdminService,
) {
const temporg = this.sessionStorage.getItem('organization') as Org.AsObject;
if (temporg) {
this.org = temporg;
}
this.sub = this.route.data.pipe(switchMap(data => {
this.serviceType = data.serviceType;
if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.managementService = this.injector.get(ManagementService as Type<ManagementService>);
}
return this.route.params;
})).subscribe(_ => {
this.fetchData();
});
}
public ngOnDestroy(): void {
this.sub.unsubscribe();
}
public fetchData(): void {
this.getData().then(data => {
if (data) {
this.iamData = data.toObject();
}
});
}
private async getData(): Promise<AdminOrgIamPolicyView | MgmtOrgIamPolicyView | undefined> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return this.managementService.GetMyOrgIamPolicy();
case PolicyComponentServiceType.ADMIN:
if (this.org?.id) {
return this.adminService.GetOrgIamPolicy(this.org.id);
}
break;
}
}
public savePolicy(): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
if ((this.iamData as MgmtOrgIamPolicyView.AsObject).pb_default) {
this.adminService.CreateOrgIamPolicy(
this.org.id,
this.iamData.userLoginMustBeDomain,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
break;
} else {
this.adminService.UpdateOrgIamPolicy(
this.org.id,
this.iamData.userLoginMustBeDomain,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
break;
}
case PolicyComponentServiceType.ADMIN:
// update Default org iam policy?
this.adminService.UpdateOrgIamPolicy(
this.org.id,
this.iamData.userLoginMustBeDomain,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
break;
}
}
public removePolicy(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.adminService.RemoveOrgIamPolicy(this.org.id).then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
setTimeout(() => {
this.fetchData();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
}
public get isDefault(): boolean {
if (this.iamData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.iamData as MgmtOrgIamPolicyView.AsObject).pb_default;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,34 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { OrgIamPolicyRoutingModule } from './org-iam-policy-routing.module';
import { OrgIamPolicyComponent } from './org-iam-policy.component';
@NgModule({
declarations: [OrgIamPolicyComponent],
imports: [
OrgIamPolicyRoutingModule,
CommonModule,
FormsModule,
MatInputModule,
MatFormFieldModule,
MatButtonModule,
MatSlideToggleModule,
MatIconModule,
HasRoleModule,
MatTooltipModule,
TranslateModule,
DetailLayoutModule,
],
})
export class OrgIamPolicyModule { }

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PolicyComponentAction } from '../policy-component-action.enum';
import { PasswordAgePolicyComponent } from './password-age-policy.component';
const routes: Routes = [
@@ -10,15 +9,6 @@ const routes: Routes = [
component: PasswordAgePolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.MODIFY,
},
},
{
path: 'create',
component: PasswordAgePolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.CREATE,
},
},
];

View File

@@ -1,19 +1,15 @@
<app-detail-layout [backRouterLink]="[ '/org']" [title]="title ? (title | translate) : ''"
[description]="desc ? (desc | translate) : ''">
<ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn" (click)="deletePolicy()"
mat-stroked-button>
{{'ORG.POLICY.DELETE' | translate}}
<app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
[title]="'POLICY.PWD_AGE.TITLE' | translate" [description]="'POLICY.PWD_AGE.DESCRIPTION' | translate">
<ng-template appHasRole [appHasRole]="['policy.delete']">
<button *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()" mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button>
</ng-template>
<div class="content" *ngIf="ageData">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="ageData.description" required />
</mat-form-field>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.EXPIREWARNDAYS' | translate}}</span>
<span class="left-desc">{{'POLICY.DATA.EXPIREWARNDAYS' | translate}}</span>
<span class="fill-space"></span>
<div class="length-wrapper">
<button mat-icon-button (click)="incrementExpireWarnDays()">
@@ -27,7 +23,7 @@
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.MAXAGEDAYS' | translate}}</span>
<span class="left-desc">{{'POLICY.DATA.MAXAGEDAYS' | translate}}</span>
<span class="fill-space"></span>
<div class="length-wrapper">
<button mat-icon-button (click)="incrementMaxAgeDays()">

View File

@@ -1,8 +1,3 @@
button {
border-radius: .5rem;
}
.content {
padding-top: 1rem;
display: flex;
@@ -12,10 +7,10 @@ button {
.row {
display: flex;
align-items: center;
padding: .5rem 0;
padding: .3rem 0;
.left-desc {
color: #8795a1;
color: var(--grey);
font-size: .9rem;
}
@@ -26,6 +21,7 @@ button {
.length-wrapper {
display: flex;
align-items: center;
margin-right: -.6rem;
}
}
}
@@ -39,6 +35,5 @@ button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
border-radius: .5rem;
}
}

View File

@@ -1,17 +1,14 @@
import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Component, Injector, OnDestroy, Type } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import {
OrgIamPolicy,
PasswordAgePolicy,
PasswordComplexityPolicy,
PasswordLockoutPolicy,
} from 'src/app/proto/generated/management_pb';
import { DefaultPasswordAgePolicyView } from 'src/app/proto/generated/admin_pb';
import { PasswordAgePolicyView } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentAction } from '../policy-component-action.enum';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({
@@ -20,37 +17,37 @@ import { PolicyComponentAction } from '../policy-component-action.enum';
styleUrls: ['./password-age-policy.component.scss'],
})
export class PasswordAgePolicyComponent implements OnDestroy {
public title: string = '';
public desc: string = '';
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public service!: AdminService | ManagementService;
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE;
public PolicyComponentAction: any = PolicyComponentAction;
public ageData!: PasswordAgePolicy.AsObject;
public ageData!: PasswordAgePolicyView.AsObject | DefaultPasswordAgePolicyView.AsObject;
private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(
private route: ActivatedRoute,
private mgmtService: ManagementService,
private router: Router,
private toast: ToastService,
private injector: Injector,
) {
this.sub = this.route.data.pipe(switchMap(data => {
this.componentAction = data.action;
return this.route.params;
})).subscribe(params => {
this.title = 'ORG.POLICY.PWD_AGE.TITLECREATE';
this.desc = 'ORG.POLICY.PWD_AGE.DESCRIPTIONCREATE';
if (this.componentAction === PolicyComponentAction.MODIFY) {
this.getData(params).then(data => {
if (data) {
this.ageData = data.toObject() as PasswordAgePolicy.AsObject;
}
});
this.serviceType = data.serviceType;
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
return this.route.params;
})).subscribe(() => {
this.getData().then(data => {
if (data) {
this.ageData = data.toObject();
}
});
});
}
@@ -58,19 +55,28 @@ export class PasswordAgePolicyComponent implements OnDestroy {
this.sub.unsubscribe();
}
private async getData(params: any):
Promise<PasswordLockoutPolicy | PasswordAgePolicy | PasswordComplexityPolicy | OrgIamPolicy | undefined> {
this.title = 'ORG.POLICY.PWD_AGE.TITLE';
this.desc = 'ORG.POLICY.PWD_AGE.DESCRIPTION';
return this.mgmtService.GetPasswordAgePolicy();
private async getData():
Promise<PasswordAgePolicyView | DefaultPasswordAgePolicyView> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).GetPasswordAgePolicy();
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).GetDefaultPasswordAgePolicy();
}
}
public deletePolicy(): void {
this.mgmtService.DeletePasswordAgePolicy(this.ageData.id).then(() => {
this.toast.showInfo('Successfully deleted');
}).catch(error => {
this.toast.showError(error);
});
public removePolicy(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
(this.service as ManagementService).RemovePasswordAgePolicy().then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
setTimeout(() => {
this.getData();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
}
public incrementExpireWarnDays(): void {
@@ -98,29 +104,46 @@ export class PasswordAgePolicyComponent implements OnDestroy {
}
public savePolicy(): void {
if (this.componentAction === PolicyComponentAction.CREATE) {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
if ((this.ageData as PasswordAgePolicyView.AsObject).pb_default) {
(this.service as ManagementService).CreatePasswordAgePolicy(
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
} else {
(this.service as ManagementService).UpdatePasswordAgePolicy(
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
}
break;
case PolicyComponentServiceType.ADMIN:
(this.service as AdminService).UpdateDefaultPasswordAgePolicy(
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
break;
}
}
this.mgmtService.CreatePasswordAgePolicy(
this.ageData.description,
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.componentAction === PolicyComponentAction.MODIFY) {
this.mgmtService.UpdatePasswordAgePolicy(
this.ageData.description,
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
public get isDefault(): boolean {
if (this.ageData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.ageData as PasswordAgePolicyView.AsObject).pb_default;
} else {
return false;
}
}
}

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PolicyComponentAction } from '../policy-component-action.enum';
import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component';
const routes: Routes = [
@@ -10,15 +9,6 @@ const routes: Routes = [
component: PasswordComplexityPolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.MODIFY,
},
},
{
path: 'create',
component: PasswordComplexityPolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.CREATE,
},
},
];

View File

@@ -1,19 +1,22 @@
<app-detail-layout [backRouterLink]="[ '/org']" [title]="title ? (title | translate) : ''"
[description]="desc ? (desc | translate) : ''">
<ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn" (click)="deletePolicy()"
mat-stroked-button>
{{'ORG.POLICY.DELETE' | translate}}
<app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
[title]="'POLICY.PWD_COMPLEXITY.TITLE' | translate" [description]="'POLICY.PWD_COMPLEXITY.DESCRIPTION' | translate">
<p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div>
<ng-template appHasRole [appHasRole]="['policy.delete']">
<button *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()" mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button>
</ng-template>
<div *ngIf="complexityData" class="content">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="complexityData.description" required />
</mat-form-field>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.MINLENGTH' | translate}}</span>
<span class="left-desc">{{'POLICY.DATA.MINLENGTH' | translate}}</span>
<span class="fill-space"></span>
<div class="length-wrapper">
<button mat-icon-button (click)="decrementLength()">
@@ -26,26 +29,26 @@
</div>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASNUMBER' | translate}}</span>
<span class="left-desc">{{'POLICY.DATA.HASNUMBER' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl [(ngModel)]="complexityData.hasNumber">
</mat-slide-toggle>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASSYMBOL' | translate}}</span>
<span class="left-desc">{{'POLICY.DATA.HASSYMBOL' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasSymbol" ngDefaultControl [(ngModel)]="complexityData.hasSymbol">
</mat-slide-toggle>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASLOWERCASE' | translate}}</span>
<span class="left-desc">{{'POLICY.DATA.HASLOWERCASE' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasLowercase" ngDefaultControl
[(ngModel)]="complexityData.hasLowercase">
</mat-slide-toggle>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASUPPERCASE' | translate}}</span>
<span class="left-desc">{{'POLICY.DATA.HASUPPERCASE' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasUppercase" ngDefaultControl
[(ngModel)]="complexityData.hasUppercase">
@@ -54,7 +57,7 @@
</div>
<div class="btn-container">
<button (click)="savePolicy()" color="primary" type="submit" [disabled]="!complexityData?.description"
<button (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</app-detail-layout>

View File

@@ -1,6 +1,10 @@
.default {
color: #5282c1;
margin-top: 0;
}
button {
border-radius: .5rem;
.spinner-wr {
margin: .5rem 0;
}
.content {
@@ -12,10 +16,9 @@ button {
.row {
display: flex;
align-items: center;
padding: .5rem 0;
padding: .3rem 0;
.left-desc {
color: #8795a1;
font-size: .9rem;
}
@@ -26,6 +29,7 @@ button {
.length-wrapper {
display: flex;
align-items: center;
margin-right: -.6rem;
}
}
}
@@ -39,6 +43,5 @@ button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
border-radius: .5rem;
}
}

View File

@@ -1,17 +1,14 @@
import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Component, Injector, OnDestroy, Type } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import {
OrgIamPolicy,
PasswordAgePolicy,
PasswordComplexityPolicy,
PasswordLockoutPolicy,
} from 'src/app/proto/generated/management_pb';
import { DefaultPasswordComplexityPolicy } from 'src/app/proto/generated/admin_pb';
import { PasswordComplexityPolicyView } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentAction } from '../policy-component-action.enum';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({
selector: 'app-password-policy',
@@ -19,36 +16,45 @@ import { PolicyComponentAction } from '../policy-component-action.enum';
styleUrls: ['./password-complexity-policy.component.scss'],
})
export class PasswordComplexityPolicyComponent implements OnDestroy {
public title: string = '';
public desc: string = '';
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public service!: ManagementService | AdminService;
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE;
public PolicyComponentAction: any = PolicyComponentAction;
public complexityData!: PasswordComplexityPolicy.AsObject;
public complexityData!: PasswordComplexityPolicyView.AsObject | DefaultPasswordComplexityPolicy.AsObject;
private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public loading: boolean = false;
constructor(
private route: ActivatedRoute,
private mgmtService: ManagementService,
private router: Router,
private toast: ToastService,
private injector: Injector,
) {
this.sub = this.route.data.pipe(switchMap(data => {
this.componentAction = data.action;
return this.route.params;
})).subscribe(params => {
this.title = 'ORG.POLICY.PWD_COMPLEXITY.TITLECREATE';
this.desc = 'ORG.POLICY.PWD_COMPLEXITY.DESCRIPTIONCREATE';
this.serviceType = data.serviceType;
if (this.componentAction === PolicyComponentAction.MODIFY) {
this.getData(params).then(data => {
if (data) {
this.complexityData = data.toObject() as PasswordComplexityPolicy.AsObject;
}
});
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
return this.route.params;
})).subscribe(() => {
this.fetchData();
});
}
public fetchData(): void {
this.loading = true;
this.getData().then(data => {
if (data) {
this.complexityData = data.toObject();
this.loading = false;
}
});
}
@@ -57,19 +63,27 @@ export class PasswordComplexityPolicyComponent implements OnDestroy {
this.sub.unsubscribe();
}
private async getData(params: any):
Promise<PasswordLockoutPolicy | PasswordAgePolicy | PasswordComplexityPolicy | OrgIamPolicy | undefined> {
this.title = 'ORG.POLICY.PWD_COMPLEXITY.TITLE';
this.desc = 'ORG.POLICY.PWD_COMPLEXITY.DESCRIPTION';
return this.mgmtService.GetPasswordComplexityPolicy();
private async getData():
Promise<PasswordComplexityPolicyView | DefaultPasswordComplexityPolicy> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).GetPasswordComplexityPolicy();
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).GetDefaultPasswordComplexityPolicy();
}
}
public deletePolicy(): void {
this.mgmtService.DeletePasswordComplexityPolicy(this.complexityData.id).then(() => {
this.toast.showInfo('Successfully deleted');
}).catch(error => {
this.toast.showError(error);
});
public removePolicy(): void {
if (this.service instanceof ManagementService) {
this.service.removePasswordComplexityPolicy().then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
setTimeout(() => {
this.fetchData();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
}
public incrementLength(): void {
@@ -85,35 +99,56 @@ export class PasswordComplexityPolicyComponent implements OnDestroy {
}
public savePolicy(): void {
if (this.componentAction === PolicyComponentAction.CREATE) {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
if ((this.complexityData as PasswordComplexityPolicyView.AsObject).pb_default) {
(this.service as ManagementService).CreatePasswordComplexityPolicy(
this.mgmtService.CreatePasswordComplexityPolicy(
this.complexityData.description,
this.complexityData.hasLowercase,
this.complexityData.hasUppercase,
this.complexityData.hasNumber,
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
this.complexityData.hasLowercase,
this.complexityData.hasUppercase,
this.complexityData.hasNumber,
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
} else {
(this.service as ManagementService).UpdatePasswordComplexityPolicy(
this.complexityData.hasLowercase,
this.complexityData.hasUppercase,
this.complexityData.hasNumber,
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
}
break;
case PolicyComponentServiceType.ADMIN:
(this.service as AdminService).UpdateDefaultPasswordComplexityPolicy(
this.complexityData.hasLowercase,
this.complexityData.hasUppercase,
this.complexityData.hasNumber,
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
break;
}
}
} else if (this.componentAction === PolicyComponentAction.MODIFY) {
this.mgmtService.UpdatePasswordComplexityPolicy(
this.complexityData.description,
this.complexityData.hasLowercase,
this.complexityData.hasUppercase,
this.complexityData.hasNumber,
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
public get isDefault(): boolean {
if (this.complexityData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.complexityData as PasswordComplexityPolicyView.AsObject).pb_default;
} else {
return false;
}
}
}

View File

@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
@@ -29,6 +30,7 @@ import { PasswordComplexityPolicyComponent } from './password-complexity-policy.
MatTooltipModule,
TranslateModule,
DetailLayoutModule,
MatProgressSpinnerModule,
],
})
export class PasswordComplexityPolicyModule { }

View File

@@ -1,30 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PolicyComponentAction } from '../policy-component-action.enum';
import { PasswordIamPolicyComponent } from './password-iam-policy.component';
const routes: Routes = [
{
path: '',
component: PasswordIamPolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.MODIFY,
},
},
{
path: 'create',
component: PasswordIamPolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.CREATE,
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class PasswordIamPolicyRoutingModule { }

View File

@@ -1,28 +0,0 @@
<app-detail-layout [backRouterLink]="[ '/org']" [title]="title ? (title | translate) : ''"
[description]="desc ? (desc | translate) : ''">
<!-- <ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn" (click)="deletePolicy()"
mat-stroked-button>
{{'ORG.POLICY.DELETE' | translate}}
</button>
</ng-template> -->
<div class="content" *ngIf="iamData">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="iamData.description" required />
</mat-form-field>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.USERLOGINMUSTBEDOMAIN' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
[(ngModel)]="iamData.userLoginMustBeDomain">
</mat-slide-toggle>
</div>
</div>
<div class="btn-container">
<button (click)="savePolicy()" color="primary" type="submit" [disabled]="!iamData?.description"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</app-detail-layout>

View File

@@ -1,102 +0,0 @@
import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import {
OrgIamPolicy,
PasswordAgePolicy,
PasswordComplexityPolicy,
PasswordLockoutPolicy,
} from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentAction } from '../policy-component-action.enum';
@Component({
selector: 'app-password-iam-policy',
templateUrl: './password-iam-policy.component.html',
styleUrls: ['./password-iam-policy.component.scss'],
})
export class PasswordIamPolicyComponent implements OnDestroy {
public title: string = '';
public desc: string = '';
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE;
public PolicyComponentAction: any = PolicyComponentAction;
public iamData!: OrgIamPolicy.AsObject;
private sub: Subscription = new Subscription();
constructor(
private route: ActivatedRoute,
private mgmtService: ManagementService,
private adminService: AdminService,
private router: Router,
private toast: ToastService,
private sessionStorage: StorageService,
) {
this.sub = this.route.data.pipe(switchMap(data => {
this.componentAction = data.action;
console.log(data.action);
return this.route.params;
})).subscribe(params => {
this.title = 'ORG.POLICY.IAM_POLICY.TITLECREATE';
this.desc = 'ORG.POLICY.IAM_POLICY.DESCRIPTIONCREATE';
if (this.componentAction === PolicyComponentAction.MODIFY) {
this.getData(params).then(data => {
if (data) {
this.iamData = data.toObject() as OrgIamPolicy.AsObject;
}
});
}
});
}
public ngOnDestroy(): void {
this.sub.unsubscribe();
}
private async getData(params: any):
Promise<PasswordLockoutPolicy | PasswordAgePolicy | PasswordComplexityPolicy | OrgIamPolicy | undefined> {
this.title = 'ORG.POLICY.IAM_POLICY.TITLECREATE';
this.desc = 'ORG.POLICY.IAM_POLICY.DESCRIPTIONCREATE';
return this.mgmtService.GetMyOrgIamPolicy();
}
public savePolicy(): void {
if (this.componentAction === PolicyComponentAction.CREATE) {
const orgId = this.sessionStorage.getItem('organization');
if (orgId) {
this.adminService.CreateOrgIamPolicy(
orgId,
this.iamData.description,
this.iamData.userLoginMustBeDomain,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
}
} else if (this.componentAction === PolicyComponentAction.MODIFY) {
const orgId = this.sessionStorage.getItem('organization');
if (orgId) {
this.adminService.UpdateOrgIamPolicy(
orgId,
this.iamData.description,
this.iamData.userLoginMustBeDomain,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
}
}
}
}

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PolicyComponentAction } from '../policy-component-action.enum';
import { PasswordLockoutPolicyComponent } from './password-lockout-policy.component';
const routes: Routes = [
@@ -10,15 +9,6 @@ const routes: Routes = [
component: PasswordLockoutPolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.MODIFY,
},
},
{
path: 'create',
component: PasswordLockoutPolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.CREATE,
},
},
];

View File

@@ -1,19 +1,17 @@
<app-detail-layout [backRouterLink]="[ '/org']" [title]="title ? (title | translate) : ''"
[description]="desc ? (desc | translate) : ''">
<ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn" (click)="deletePolicy()"
mat-stroked-button>
{{'ORG.POLICY.DELETE' | translate}}
<app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
[title]="'POLICY.PWD_LOCKOUT.TITLE' | translate" [description]="'POLICY.PWD_LOCKOUT.DESCRIPTION' | translate">
<p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
<ng-template appHasRole [appHasRole]="['policy.delete']">
<button *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()" mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button>
</ng-template>
<div class="content" *ngIf="lockoutData">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="lockoutData.description" required />
</mat-form-field>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.MAXATTEMPTS' | translate}}</span>
<span class="left-desc">{{'POLICY.DATA.MAXATTEMPTS' | translate}}</span>
<span class="fill-space"></span>
<div class="length-wrapper">
<button mat-icon-button (click)="incrementMaxAttempts()">
@@ -26,10 +24,10 @@
</div>
</div>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.SHOWLOCKOUTFAILURES' | translate}}</span>
<span class="left-desc">{{'POLICY.DATA.SHOWLOCKOUTFAILURES' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="showLockOutFailures" ngDefaultControl
[(ngModel)]="lockoutData.showLockOutFailures">
<mat-slide-toggle color="primary" name="showLockoutFailure" ngDefaultControl
[(ngModel)]="lockoutData.showLockoutFailure">
</mat-slide-toggle>
</div>
</div>

View File

@@ -1,6 +1,6 @@
button {
border-radius: .5rem;
.default {
color: #5282c1;
margin-top: 0;
}
.content {
@@ -12,10 +12,9 @@ button {
.row {
display: flex;
align-items: center;
padding: .5rem 0;
padding: .3rem 0;
.left-desc {
color: #8795a1;
font-size: .9rem;
}
@@ -39,6 +38,5 @@ button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
border-radius: .5rem;
}
}

View File

@@ -1,18 +1,15 @@
import { Component, OnDestroy } from '@angular/core';
import { Component, Injector, Input, OnDestroy, Type } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import {
OrgIamPolicy,
PasswordAgePolicy,
PasswordComplexityPolicy,
PasswordLockoutPolicy,
} from 'src/app/proto/generated/management_pb';
import { DefaultPasswordLockoutPolicyView } from 'src/app/proto/generated/admin_pb';
import { PasswordLockoutPolicyView } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentAction } from '../policy-component-action.enum';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({
selector: 'app-password-lockout-policy',
@@ -20,37 +17,35 @@ import { PolicyComponentAction } from '../policy-component-action.enum';
styleUrls: ['./password-lockout-policy.component.scss'],
})
export class PasswordLockoutPolicyComponent implements OnDestroy {
public title: string = '';
public desc: string = '';
@Input() public service!: ManagementService | AdminService;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE;
public PolicyComponentAction: any = PolicyComponentAction;
public lockoutForm!: FormGroup;
public lockoutData!: PasswordLockoutPolicy.AsObject;
public lockoutData!: PasswordLockoutPolicyView.AsObject;
private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(
private route: ActivatedRoute,
private mgmtService: ManagementService,
private router: Router,
private toast: ToastService,
private injector: Injector,
) {
this.sub = this.route.data.pipe(switchMap(data => {
this.componentAction = data.action;
return this.route.params;
})).subscribe(params => {
this.title = 'ORG.POLICY.PWD_LOCKOUT.TITLECREATE';
this.desc = 'ORG.POLICY.PWD_LOCKOUT.DESCRIPTIONCREATE';
this.serviceType = data.serviceType;
if (this.componentAction === PolicyComponentAction.MODIFY) {
this.getData(params).then(data => {
if (data) {
this.lockoutData = data.toObject() as PasswordLockoutPolicy.AsObject;
}
});
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
return this.route.params;
})).subscribe(() => {
this.fetchData();
});
}
@@ -58,20 +53,32 @@ export class PasswordLockoutPolicyComponent implements OnDestroy {
this.sub.unsubscribe();
}
private async getData(params: any):
Promise<PasswordLockoutPolicy | PasswordAgePolicy | PasswordComplexityPolicy | OrgIamPolicy | undefined> {
this.title = 'ORG.POLICY.PWD_LOCKOUT.TITLE';
this.desc = 'ORG.POLICY.PWD_LOCKOUT.DESCRIPTION';
return this.mgmtService.GetPasswordLockoutPolicy();
private fetchData(): void {
this.getData().then(data => {
if (data) {
this.lockoutData = data.toObject() as PasswordLockoutPolicyView.AsObject;
}
});
}
public deletePolicy(): void {
this.mgmtService.DeletePasswordLockoutPolicy(this.lockoutData.id).then(() => {
this.toast.showInfo('Successfully deleted');
}).catch(error => {
this.toast.showError(error);
});
private getData(): Promise<PasswordLockoutPolicyView | DefaultPasswordLockoutPolicyView> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).GetPasswordLockoutPolicy();
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).GetDefaultPasswordLockoutPolicy();
}
}
public removePolicy(): void {
if (this.service instanceof ManagementService) {
this.service.RemovePasswordLockoutPolicy().then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
this.fetchData();
}).catch(error => {
this.toast.showError(error);
});
}
}
public incrementMaxAttempts(): void {
@@ -87,27 +94,44 @@ export class PasswordLockoutPolicyComponent implements OnDestroy {
}
public savePolicy(): void {
if (this.componentAction === PolicyComponentAction.CREATE) {
this.mgmtService.CreatePasswordLockoutPolicy(
this.lockoutData.description,
let promise: Promise<any>;
if (this.service instanceof AdminService) {
promise = this.service.UpdateDefaultPasswordLockoutPolicy(
this.lockoutData.maxAttempts,
this.lockoutData.showLockOutFailures,
this.lockoutData.showLockoutFailure,
).then(() => {
this.router.navigate(['org']);
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.componentAction === PolicyComponentAction.MODIFY) {
} else {
if ((this.lockoutData as PasswordLockoutPolicyView.AsObject).pb_default) {
promise = this.service.CreatePasswordLockoutPolicy(
this.lockoutData.maxAttempts,
this.lockoutData.showLockoutFailure,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
} else {
promise = this.service.UpdatePasswordLockoutPolicy(
this.lockoutData.maxAttempts,
this.lockoutData.showLockoutFailure,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
}
}
}
this.mgmtService.UpdatePasswordLockoutPolicy(
this.lockoutData.description,
this.lockoutData.maxAttempts,
this.lockoutData.showLockOutFailures,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
public get isDefault(): boolean {
if (this.lockoutData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.lockoutData as PasswordLockoutPolicyView.AsObject).pb_default;
} else {
return false;
}
}
}

View File

@@ -1,5 +0,0 @@
export enum PolicyComponentAction {
CREATE = 'create',
MODIFY = 'modify',
}

View File

@@ -4,6 +4,7 @@ export enum PolicyComponentType {
COMPLEXITY = 'complexity',
IAM = 'iam',
LOGIN = 'login',
LABEL = 'label',
}
export enum PolicyComponentServiceType {
MGMT = 'mgmt',

View File

@@ -0,0 +1,109 @@
<h1>{{'POLICY.TITLE' | translate}}</h1>
<p class="top-desc">{{'POLICY.DESCRIPTION' | translate}}</p>
<div class="row-lyt">
<ng-template appHasRole
[appHasRole]="PolicyGridType.IAM ? ['iam.policy.read'] : PolicyGridType.ORG ? ['policy.read'] : []">
<div class="p-item card">
<div class="avatar">
<mat-icon class="icon" svgIcon="mdi_textbox_password"></mat-icon>
</div>
<div class="title">
<span>{{'POLICY.PWD_COMPLEXITY.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<i class="icon las la-check-circle"></i>
</button>
</div>
<p class="desc">
{{'POLICY.PWD_COMPLEXITY.DESCRIPTION' | translate}}</p>
<span class="fill-space"></span>
<div class="btn-wrapper">
<button [routerLink]="[ 'policy', PolicyComponentType.COMPLEXITY ]"
mat-stroked-button>{{'POLICY.BTN_EDIT' | translate}}</button>
</div>
</div>
</ng-template>
<ng-template appHasRole [appHasRole]="['iam.policy.read']">
<div class="p-item card">
<div class="avatar">
<i class="icon las la-gem"></i>
</div>
<div class="title">
<span>{{'POLICY.IAM_POLICY.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<i class="icon las la-check-circle"></i>
</button>
</div>
<p class="desc">
{{'POLICY.IAM_POLICY.DESCRIPTION' | translate}}</p>
<span class="fill-space"></span>
<div class="btn-wrapper">
<ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button [routerLink]="[ 'policy', PolicyComponentType.IAM ]"
mat-stroked-button>{{'POLICY.BTN_EDIT' | translate}}</button>
</ng-template>
</div>
</div>
</ng-template>
<ng-template appHasRole
[appHasRole]="PolicyGridType.IAM ? ['iam.policy.read'] : PolicyGridType.ORG ? ['policy.read'] : []">
<div class="p-item card">
<div class="avatar">
<i class="icon las la-sign-in-alt"></i>
</div>
<div class="title">
<span>{{'POLICY.LOGIN_POLICY.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<i class="icon las la-check-circle"></i>
</button>
</div>
<ng-template #showDescIAM>
<p class="desc">
{{'POLICY.LOGIN_POLICY.DESCRIPTION' | translate}}</p>
</ng-template>
<span class="fill-space"></span>
<div class="btn-wrapper">
<ng-template appHasRole [appHasRole]="['policy.write']">
<button [routerLink]="[ 'policy', PolicyComponentType.LOGIN ]"
mat-stroked-button>{{'POLICY.BTN_EDIT' | translate}}</button>
</ng-template>
</div>
</div>
</ng-template>
<ng-container *ngIf="type === PolicyGridType.IAM">
<ng-template appHasRole [appHasRole]="['iam.policy.read']">
<div class="p-item card">
<div class="avatar">
<i class="icon las la-envelope"></i>
</div>
<div class="title">
<span>{{'POLICY.LABEL.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<i class="icon las la-check-circle"></i>
</button>
</div>
<p class="desc">
{{'POLICY.LABEL.DESCRIPTION' | translate}}</p>
<span class="fill-space"></span>
<div class="btn-wrapper">
<ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button [routerLink]="[ 'policy', PolicyComponentType.LABEL ]"
mat-stroked-button>{{'POLICY.BTN_EDIT' | translate}}</button>
</ng-template>
</div>
</div>
</ng-template>
</ng-container>
</div>

View File

@@ -3,17 +3,17 @@ h1 {
}
.top-desc {
color: #8795a1;
color: var(--grey);
}
.row-lyt {
display: flex;
flex-wrap: wrap;
margin: 0 -1rem;
margin: 0 -.5rem;
.p-item {
flex-basis: 300px;
margin: 1rem;
flex-basis: 290px;
margin: .5rem;
display: flex;
flex-direction: column;
min-height: 200px;
@@ -38,6 +38,7 @@ h1 {
font-size: 2.5rem;
height: 2.5rem;
line-height: 2.5rem;
color: white;
}
}
@@ -46,7 +47,7 @@ h1 {
align-items: center;
span {
font-size: 1.2rem;
font-size: 1.1rem;
}
.icon {
@@ -56,8 +57,8 @@ h1 {
}
.desc {
font-size: .9rem;
color: #8795a1;
font-size: 14px;
color: var(--grey);
}
.fill-space {
@@ -70,7 +71,6 @@ h1 {
button {
margin-right: 1rem;
border-radius: .5rem;
}
}
}

View File

@@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core';
import { PolicyComponentType } from 'src/app/modules/policies/policy-component-types.enum';
export enum PolicyGridType {
ORG,
IAM,
}
@Component({
selector: 'app-policy-grid',
templateUrl: './policy-grid.component.html',
styleUrls: ['./policy-grid.component.scss'],
})
export class PolicyGridComponent {
@Input() public type!: PolicyGridType;
public PolicyComponentType: any = PolicyComponentType;
public PolicyGridType: any = PolicyGridType;
constructor() { }
}

View File

@@ -0,0 +1,27 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { PolicyGridComponent } from './policy-grid.component';
@NgModule({
declarations: [PolicyGridComponent],
imports: [
CommonModule,
HasRolePipeModule,
HasRoleModule,
TranslateModule,
RouterModule,
MatButtonModule,
MatIconModule,
],
exports: [
PolicyGridComponent,
],
})
export class PolicyGridModule { }

View File

@@ -1,96 +1,26 @@
<app-detail-layout *ngIf="project" [backRouterLink]="[ '/projects', project?.projectId]"
title="{{projectName}} {{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}">
<app-refresh-table *ngIf="project" (refreshed)="changePage()" [dataSize]="dataSource.totalResult"
[timestamp]="dataSource.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async">
<ng-template appHasRole actions
<app-members-table *ngIf="project" [dataSource]="dataSource" [memberRoleOptions]="memberRoleOptions"
(updateRoles)="updateRoles($event.member, $event.change)" [factoryLoadFunc]="changePageFactory"
(changedSelection)="selection = $event" [refreshTrigger]="changePage"
[canWrite]="['project.member.write$', 'project.member.write:'+ project.projectId] | hasRole | async"
[canDelete]="['project.member.delete$', 'project.member.delete:'+project.projectId] | hasRole | async"
(deleteMember)="removeProjectMember($event)">
<ng-template appHasRole selectactions
[appHasRole]="['project.member.delete:' + project.projectId, 'project.member.delete']">
<button (click)="removeProjectMemberSelection()" color="warn"
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button" mat-icon-button
*ngIf="selection.hasValue()">
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="del-button" mat-raised-button>
<i class="las la-trash"></i>
{{'ACTIONS.SELECTIONDELETE' | translate}}
</button>
</ng-template>
<ng-template appHasRole actions
<ng-template appHasRole writeactions
[appHasRole]="['project.member.write:'+project.projectId,'project.member.write']">
<a color="primary" [disabled]="disabled" class="add-button" (click)="openAddMember()" color="primary"
mat-raised-button>
<a color="primary" (click)="openAddMember()" color="primary" mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
</ng-template>
<div class="table-wrapper">
<table mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="userId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERID' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.userId}} </td>
</ng-container>
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.FIRSTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LASTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.lastName}} </td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.EMAIL' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.email}}
</td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'ROLESLABEL' | translate }} </th>
<td mat-cell *matCellDef="let member">
<mat-form-field class="form-field" appearance="outline" *ngIf="project">
<mat-label>{{ 'ROLESLABEL' | translate }}</mat-label>
<mat-select [(ngModel)]="member.rolesList" multiple
[disabled]="([('project.member.write:' + project.projectId), 'project.member.write'] | hasRole | async) == false"
(selectionChange)="updateRoles(member, $event)">
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
{{ role }}
</mat-option>
</mat-select>
</mat-form-field>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
<mat-paginator *ngIf="dataSource" class="paginator" #paginator [pageSize]="INITIALPAGESIZE"
[length]="dataSource.totalResult" [pageSizeOptions]="[25, 50, 100, 250]" (page)="changePage($event)">
</mat-paginator>
</div>
</app-refresh-table>
</app-detail-layout>
</app-members-table>
</app-detail-layout>
<!-- TODO: check for both project.member and project.grant.member permissions -->

View File

@@ -1,50 +1,7 @@
.icon-button {
.del-button {
margin-right: .5rem;
}
.add-button {
border-radius: .5rem;
}
.table-wrapper {
overflow-x: auto;
.table,
.paginator {
width: 100%;
td,
th {
padding: .5rem;
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
.action {
width: 40px;
}
.data-row {
&:hover {
background-color: #ffffff05;
}
}
.selection {
width: 50px;
max-width: 50px;
}
}
}
.pointer {
outline: none;
cursor: pointer;
:root {
width: 100%;
}

Some files were not shown because too many files have changed in this diff Show More