Compare commits

...

108 Commits

Author SHA1 Message Date
775ac8b474 Make /oidc/jwks support ANY http verb
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-12 23:26:02 -05:00
3d6908b7ec First steps for webauthn 2023-01-24 18:00:32 -06:00
0b8c4b87df Drone: only rollout on tag/promotion
All checks were successful
continuous-integration/drone Build is passing
2022-12-03 22:28:25 -06:00
1b12af0cd2 Drone: fix deploy dir
All checks were successful
continuous-integration/drone Build is passing
2022-12-02 10:34:23 -06:00
49be0887d0 Drone: rework ci pipeline
Some checks failed
continuous-integration/drone Build is failing
2022-12-02 10:24:35 -06:00
d63de520c9 Implement better radius support 2022-10-26 13:45:05 -05:00
0d24782691 Update Dockerfile to Node.js 16 2022-10-26 03:07:36 -05:00
35113ed81c Remove Vault support; fix OpenID Connect client delete issue 2022-10-26 02:59:43 -05:00
562ada3af5 Add kubernetes deployment specs 2022-06-25 20:55:06 -05:00
04ea16743d Include profile photo in user API data
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2022-01-27 22:45:11 -06:00
cf91063315 #9 - show app password use date in profile
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2021-12-14 16:40:15 -06:00
fd8a05446a Report OIDC errors to output
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-03 02:45:02 -06:00
dae06aa577 Disable radius 2021-11-22 09:08:33 -06:00
ffbcf1b514 More RADIUS work 2021-11-22 09:08:22 -06:00
6e161dd383 Radius server - fail gracefully if port in use
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-24 13:39:11 -05:00
64bc167d01 RADIUS - improve logging
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-24 13:37:21 -05:00
bd69be7137 Implement RADIUS server!
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-24 13:12:58 -05:00
f98f35f626
Update flitter-auth
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-14 23:53:20 -05:00
cf1ea362cd
Update flitter-auth & fix state search params
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-14 23:38:35 -05:00
7f338325b9
Update flitter-auth
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-14 23:32:38 -05:00
a60af453ab Update 'app/controllers/auth/Oauth2.controller.js'
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-15 04:25:03 +00:00
670b9b1299 Update 'app/controllers/auth/Oauth2.controller.js'
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-15 04:16:01 +00:00
5f0d67d525 Update 'app/controllers/auth/Oauth2.controller.js'
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-15 04:12:59 +00:00
1852be4ef0 Update 'app/controllers/auth/Oauth2.controller.js'
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-15 04:01:48 +00:00
54258ecb8b
Update flitter-auth
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-14 22:52:12 -05:00
1e80da9b80
Make User.to_api include combined name field
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-14 20:43:42 -05:00
5420cf58bd
Add flat user endpoint
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-14 20:42:45 -05:00
159fdb15e6
Update flitter-auth
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-14 20:37:56 -05:00
6612eb7b10 Upload files to 'app/assets'
All checks were successful
continuous-integration/drone Build is passing
2021-09-06 17:15:41 +00:00
2fe1d499f6
Fix format of SAML middleware error throw
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2021-08-22 20:06:36 -05:00
cb783ea277
Revert: SAML request middleware - pass error, not string
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-22 20:05:21 -05:00
2f2d38d12f
SAML request middleware - pass error, not string
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-22 20:04:42 -05:00
62c818dc8d
Add ability to require e-mail verification
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2021-05-04 11:28:55 -05:00
f45e92af1e
Make coreid SPA
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-05-04 10:18:53 -05:00
ced3a15d00
Remove vault menu item
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-04 09:27:33 -05:00
9729de47f8
Make registration carry flow all the way through
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-05-03 20:06:51 -05:00
5bc98e6568
Update libflitter
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-04-23 10:59:54 -05:00
de20dce735
Add public endpoint to get user photo
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-04-19 13:32:43 -05:00
13af63a364 Update 'app/views/welcome.pug'
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-04-17 02:40:42 +00:00
3730ddc2f2
Add basic logic for managing vaults
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-04-15 15:34:13 -05:00
5391c7c6d6
Check app ID on oidc auth
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-04-15 14:06:25 -05:00
ae85c3fd24
fix format of oidc authorization checks
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-04-15 14:03:14 -05:00
3301a48750
Track oidc authorizations by app, not client
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-04-15 13:50:48 -05:00
d1312fe627
Add logic to save OpenID connect grants
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-04-15 13:41:13 -05:00
bd6eaceaf3
Remove debugging
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-15 13:16:19 -05:00
6b2257ae33
Fix recursive method call in sudo controller
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-15 13:09:56 -05:00
636e1f8ab9
Fix recursive method call in sudo controller
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-15 13:02:18 -05:00
627499537d
Look up IAM from sudo hosts
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-15 13:00:13 -05:00
7e3f198c04
Debugging
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-15 12:51:23 -05:00
b26519ea88
Make sudo access managed via IAM rather than group checkmark
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-04-15 10:56:11 -05:00
f2995899ec
Add ability to manage and grant IAM permissions as policy
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-15 10:38:43 -05:00
5645e8fae1
Clean up IAM to allow relations w/o explicit definitions
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-15 09:55:25 -05:00
a7ed5d09f1
Better IAM denial logging
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-16 15:24:21 -05:00
3a91417db3
Add default user to allow for default groups and IAM
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-03-15 17:45:18 -05:00
0844da594e
Show iam filter for machines
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-03-15 17:13:09 -05:00
64ad8931f3
Show iamTarget in relevant forms
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-03-15 16:55:50 -05:00
a9d7b1c047
Allow IAM policy to manage user access to machines & machine groups
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-15 16:28:42 -05:00
d6e4ea2e56
Add ability to manage computers and computer groups from web interface
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-15 16:10:23 -05:00
718414d924
Clean up sudo LDAP formatting
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-03-10 23:53:22 -06:00
943c30fa96
Add support for sudo
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-03-10 23:43:16 -06:00
3d2c4c0fec
Cast gidNumber to strings
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-03-10 23:21:33 -06:00
8b8c2e076f
User model - fix LDAP gidNumber
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-03-10 23:16:04 -06:00
0ee36dc429
User - resolve posix groups for all member groups
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-10 20:15:04 -06:00
48f5b3f71a
Make all groups appear in LDAP, get posix GIDs 2021-03-10 20:12:06 -06:00
ef819b0a2e
Groups - allow flagging group as su equivalent
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-10 20:06:43 -06:00
91fc8a65a2
Allow users to set login shell in profile
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-10 19:43:51 -06:00
2d31eaa148
LDAP - properly cast gidNumber to string
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-10 19:21:41 -06:00
82e25ccef0
LDAP - support posixGroups in group model
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-10 19:12:46 -06:00
53a1662f70
LDAP - specify user homeDirectory
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-10 18:37:32 -06:00
dbb8684f68
LDAP - set default loginShell
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-10 18:31:43 -06:00
6a4f82611b
LDAP - allow specifying certificate file
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-10 18:22:51 -06:00
e6a7070589
Remove duplicate uid/gid number LDAP attrs
All checks were successful
continuous-integration/drone/push Build is passing
2021-03-10 16:26:59 -06:00
e6588b4f5b
LDAP - include gid number
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-03-10 16:02:18 -06:00
20e723f39f
LDAP - cast modifications to support posix logins
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-03-10 15:48:27 -06:00
a8729930e6
Update drone to use promotions for prod
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2020-12-08 22:21:54 -06:00
c725f14bf2
Update flitter jobs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-12-04 20:49:37 -06:00
9b5216431d
Make jobs use built-in job logging
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-12-04 20:40:27 -06:00
1d5c00768c
Update flitter jobs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-12-03 20:00:40 -06:00
7f1c9ec9a8
Update libflitter
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-29 08:52:13 -05:00
fe0a4d5991
Update libflitter and set session max age
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-29 08:46:30 -05:00
f06ff83dce
Move all front-end public field definitions into constructors for iOS support
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-28 19:53:07 -05:00
251aa6cf97
Remove source map annotations from minified libraries
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-28 19:29:00 -05:00
60003d64d5
Add front-end error logging
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-28 19:13:13 -05:00
535dde13ff
Guarantee additional logging data object in permission middleware
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 10:19:01 -05:00
63d102296f
Fix bad logging method call names
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 09:55:26 -05:00
77d203b2b0
Add missing service injection...
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 09:53:27 -05:00
fcbf25e3ce
Check IAM policy for OAuth2 logins
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 09:51:36 -05:00
084ec7bbc1
inflate OpenID UID to case-sensitive on lookup
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-19 09:39:28 -05:00
6b3339a883
Force OpenID UID to be lowercase
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-19 09:35:49 -05:00
8f1bbfef56
OpenID - revert case insensitive session UID lookup
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 09:26:52 -05:00
e400e16ccc
OpenID - revert case insensitive cast to UID
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-19 09:22:09 -05:00
97096f619f
Make UID case-insensitive
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-18 23:27:23 -05:00
2d97b77bbf
Fix user bind error constructor
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 23:04:01 -05:00
5916222f7b
Update libflitte
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 22:11:23 -05:00
bb79d52911
Increase error stack trace limit
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-18 22:07:12 -05:00
2e05ec77c8
Increase error stack trace limit
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-18 22:06:08 -05:00
433af8261f
Add debug output
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-18 21:44:53 -05:00
59d831c61f
Update libflitter and enable database error logging
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 21:31:46 -05:00
fac3431375
Add api authorization logging
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 21:07:42 -05:00
7c8a05aa4f
Update libflitter
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 21:00:59 -05:00
1cd306157a
Update libflitter and add config to log API responses
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 20:49:19 -05:00
5eb0487c77
Allow oauth2 clients to exercise permissions independent to the user
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 20:22:10 -05:00
3f2680671b
Permission middleware log oauth client UUID
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 18:55:21 -05:00
fd06e17d7d
Use req.user to check auth instead of req.is_auth
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 18:35:55 -05:00
efdea10b14
Guarantee req.oauth in APIRoute middleware
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 17:19:26 -05:00
ce7349565e
Fix LDAP search error for individual users
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-10-18 17:06:34 -05:00
a4695a7ecd
Checkout master on prod deploy
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-09-14 17:31:59 -05:00
fcb5b58b11
Checkout prod branch on deploy
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2020-09-14 12:16:43 -05:00
148 changed files with 7229 additions and 3532 deletions

View File

@ -1,85 +1,68 @@
---
kind: pipeline kind: pipeline
name: default type: kubernetes
name: build
metadata:
labels:
pod-security.kubernetes.io/audit: privileged
services:
- name: docker daemon
image: docker:dind
privileged: true
environment:
DOCKER_TLS_CERTDIR: ""
steps: steps:
- name: release - name: container build
image: plugins/gitea-release image: docker:latest
settings: privileged: true
api_key: commands:
from_secret: gitea_api_key - "while ! docker stats --no-stream; do sleep 1; done"
base_url: https://code.garrettmills.dev - "docker build -t $DOCKER_REGISTRY/starship/coreid ."
checksum: md5 - "docker push $DOCKER_REGISTRY/starship/coreid"
title: ${DRONE_TAG} environment:
DOCKER_HOST: tcp://localhost:2375
DOCKER_REGISTRY:
from_secret: DOCKER_REGISTRY
- name: environment substitution
image: rockylinux:9.0-minimal
commands:
- microdnf install -y gettext
- cd deploy && mkdir ../deploy-subst && bash -c 'for f in *.yaml; do envsubst < $f > ../deploy-subst/$f; done'
environment:
COREID_DOMAIN:
from_secret: COREID_DOMAIN
DOCKER_REGISTRY:
from_secret: DOCKER_REGISTRY
COREID_DATABASE_HOST:
from_secret: COREID_DATABASE_HOST
COREID_DATABASE_NAME:
from_secret: COREID_DATABASE_NAME
COREID_LDAP_BASE_DC:
from_secret: COREID_LDAP_BASE_DC
COREID_REDIS_HOST:
from_secret: COREID_REDIS_HOST
COREID_SMTP_HOST:
from_secret: COREID_SMTP_HOST
when: when:
event: tag
- name: deploy to production
image: appleboy/drone-ssh
settings:
host:
from_secret: deploy_ssh_host
username:
from_secret: deploy_ssh_user
key:
from_secret: deploy_ssh_key
port:
from_secret: deploy_ssh_port
script:
- cd /home/coreid/CoreID
- git pull
- yarn install
when:
event:
- tag
- promote
- name: restart production services
image: appleboy/drone-ssh
settings:
host:
from_secret: deploy_ssh_host
username:
from_secret: deploy_ssh_admin_user
key:
from_secret: deploy_ssh_key
port:
from_secret: deploy_ssh_port
script:
- systemctl restart coreid-www
- systemctl restart coreid-jobs
when:
event:
- tag
- promote
- name: send success notifications
image: plugins/webhook
settings:
urls:
from_secret: notify_webhook_url
content_type: application/json
template: |
{
"title": "Drone-CI [Starship/CoreID]",
"message": "Build ${DRONE_BUILD_NUMBER} promoted to production.",
"priority": 4
}
when:
status: success
event:
- tag
- promote
- name: send error notifications
image: plugins/webhook
settings:
urls:
from_secret: notify_webhook_url
content_type: application/json
template: |
{
"title": "Drone-CI [Starship/CoreID]",
"message": "An error was encountered while promoting build ${DRONE_BUILD_NUMBER} to production.",
"priority": 6
}
when:
status: failure
event: event:
- tag - tag
- promote - promote
- name: k8s rollout
image: bitnami/kubectl
privileged: true
commands:
- cd deploy-subst && kubectl apply -f .
- kubectl rollout restart deployment/coreid-www -n starship
- kubectl rollout restart deployment/coreid-jobs -n starship
depends_on:
- container build
- environment substitution
when:
event:
- tag
- promote

3
.gitignore vendored
View File

@ -1,3 +1,5 @@
*.conf
# ---> Node # ---> Node
# Logs # Logs
logs logs
@ -150,3 +152,4 @@ tmp.uploads/*
!tmp.uploads/.gitkeep !tmp.uploads/.gitkeep
uploads/* uploads/*
!uploads/.gitkeep !uploads/.gitkeep
ttls-pap.conf

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM node:16
RUN mkdir /app
COPY package.json /app
COPY yarn.lock /app
RUN cd /app && yarn install
COPY . /app
RUN rm -rf /app/.env
RUN touch /app/.env
WORKDIR /app
CMD ["node", "index.js"]

View File

@ -44,6 +44,7 @@ const FlitterUnits = {
'LDAPController': require('./app/unit/LDAPControllerUnit'), 'LDAPController': require('./app/unit/LDAPControllerUnit'),
'LDAPRoutingUnit': require('./app/unit/LDAPRoutingUnit'), 'LDAPRoutingUnit': require('./app/unit/LDAPRoutingUnit'),
'OpenIDConnect' : require('./app/unit/OpenIDConnectUnit'), 'OpenIDConnect' : require('./app/unit/OpenIDConnectUnit'),
'Radius' : require('./app/unit/RadiusUnit'),
/* /*
* The Core Flitter Units * The Core Flitter Units

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -38,14 +38,18 @@ export default class MFAChallengePage extends Component {
static get props() { return ['app_name'] } static get props() { return ['app_name'] }
static get template() { return template } static get template() { return template }
loading = false constructor() {
super()
verify_code = '' this.loading = false
verify_success = false
error_message = '' this.verify_code = ''
other_message = '' this.verify_success = false
t = {}
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() { async vue_on_create() {
this.t = await T( this.t = await T(

View File

@ -28,12 +28,16 @@ export default class MFADisableComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return [] } static get props() { return [] }
app_name = '' constructor() {
step = 0 super()
loading = false
error_message = '' this.app_name = ''
other_message = '' this.step = 0
t = {} this.loading = false
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() { async vue_on_create() {
this.app_name = session.get('app.name') this.app_name = session.get('app.name')

View File

@ -38,12 +38,16 @@ export default class MFARecoveryComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return ['app_name'] } static get props() { return ['app_name'] }
verify_success = false constructor() {
loading = false super()
recovery_code = ''
error_message = '' this.verify_success = false
other_message = '' this.loading = false
t = {} this.recovery_code = ''
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() { async vue_on_create() {
this.t = await T( this.t = await T(

View File

@ -61,19 +61,23 @@ export default class MFASetupPage extends Component {
static get props() { return ['app_name'] } static get props() { return ['app_name'] }
static get template() { return template } static get template() { return template }
loading = false constructor() {
step = 0 super()
qr_data = '' this.loading = false
otpauth_url = '' this.step = 0
secret = ''
verify_code = ''
verify_success = false this.qr_data = ''
this.otpauth_url = ''
this.secret = ''
this.verify_code = ''
error_message = '' this.verify_success = false
other_message = ''
t = {} this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() { async vue_on_create() {
this.t = await T( this.t = await T(

View File

@ -25,7 +25,11 @@ export default class AuthPage extends Component {
static get props() { return ['app_name', 'message', 'actions'] } static get props() { return ['app_name', 'message', 'actions'] }
static get template() { return template } static get template() { return template }
loading = false constructor() {
super()
this.loading = false
}
async action_click(index) { async action_click(index) {
this.loading = true this.loading = true

View File

@ -78,23 +78,27 @@ export default class PasswordResetComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return ['app_name'] } static get props() { return ['app_name'] }
step = 0 constructor() {
loading = false super()
has_mfa = false
error_message = '' this.step = 0
other_message = '' this.loading = false
this.has_mfa = false
step_1_valid = false this.error_message = ''
step_1_calc_time = '' this.other_message = ''
step_1_problem = ''
step_2_valid = false this.step_1_valid = false
this.step_1_calc_time = ''
this.step_1_problem = ''
password = '' this.step_2_valid = false
confirm_password = ''
t = {} this.password = ''
ready = false this.confirm_password = ''
this.t = {}
this.ready = false
}
async vue_on_create() { async vue_on_create() {
this.has_mfa = !!session.get('user.has_mfa') this.has_mfa = !!session.get('user.has_mfa')

View File

@ -63,18 +63,21 @@ export default class AuthLoginForm extends Component {
] } ] }
static get template() { return template } static get template() { return template }
username = '' constructor() {
password = '' super()
button_text = ''
step_two = false
btn_disabled = true
loading = false
error_message = ''
other_message = ''
allow_back = true
auth_user = false
t = {} this.username = ''
this.password = ''
this.button_text = ''
this.step_two = false
this.btn_disabled = true
this.loading = false
this.error_message = ''
this.other_message = ''
this.allow_back = true
this.auth_user = false
this.t = {}
}
watch_username(new_username, old_username) { watch_username(new_username, old_username) {
this.btn_disabled = !new_username this.btn_disabled = !new_username

View File

@ -98,19 +98,23 @@ export default class RegistrationFormComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return ['app_name'] } static get props() { return ['app_name'] }
loading = false constructor() {
step = 1 super()
other_message = ''
error_message = ''
message = ''
btn_disabled = true
button_text = ''
first_name = '' this.loading = false
last_name = '' this.step = 1
username = '' this.other_message = ''
email = '' this.error_message = ''
t = {} this.message = ''
this.btn_disabled = true
this.button_text = ''
this.first_name = ''
this.last_name = ''
this.username = ''
this.email = ''
this.t = {}
}
async vue_on_create() { async vue_on_create() {
// Batch-load translated phrases // Batch-load translated phrases

View File

@ -28,7 +28,7 @@ const template = `
v-if="field.type === 'display' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))" v-if="field.type === 'display' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))"
v-html="typeof field.display === 'function' ? field.display(data) : field.display" v-html="typeof field.display === 'function' ? field.display(data) : field.display"
></span> ></span>
<span v-if="field.type.startsWith('select') && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))"> <span v-if="field.type.startsWith('select') && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data, field.options))">
<label :for="uuid+field.field">{{ field.name }}</label> <label :for="uuid+field.field">{{ field.name }}</label>
<select <select
:id="uuid+field.field" :id="uuid+field.field"
@ -42,13 +42,13 @@ const template = `
<option <option
v-for="option of field.options" v-for="option of field.options"
:value="option.value" :value="option.value"
:selected="data[field.field] && data[field.field].includes(option.value)" :selected="data[field.field] && (data[field.field] === option.value || (Array.isArray(data[field.field]) && data[field.field].includes(option.value)))"
>{{ typeof option.display === 'function' ? option.display(option) : option.display }}</option> >{{ typeof option.display === 'function' ? option.display(option) : option.display }}</option>
</select> </select>
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small> <small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
</span> </span>
<span v-if="field.type === 'text' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))"> <span v-if="field.type === 'text' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
<label :for="uuid+field.field">{{ field.name }}</label> <label :for="uuid+field.field" style="display: inline">{{ field.name }} <span v-if="field.help" :title="field.help"><img src="/assets/info-circle-solid.svg" height="18"></span></label>
<input <input
type="text" type="text"
class="form-control" class="form-control"
@ -146,20 +146,24 @@ export default class FormComponent extends Component {
return ['resource', 'form_id', 'initial_mode'] return ['resource', 'form_id', 'initial_mode']
} }
definition = {} constructor() {
data = {} super()
uuid = ''
title = ''
error_message = ''
other_message = ''
access_msg = '' this.definition = {}
can_access = false this.data = {}
this.uuid = ''
this.title = ''
this.error_message = ''
this.other_message = ''
is_ready = false this.access_msg = ''
mode = '' this.can_access = false
id = ''
t = {} this.is_ready = false
this.mode = ''
this.id = ''
this.t = {}
}
reset() { reset() {
this.definition = {} this.definition = {}

View File

@ -65,13 +65,17 @@ export default class ListingComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return ['resource'] } static get props() { return ['resource'] }
definition = {} constructor() {
data = [] super()
resource_class = {}
access_msg = '' this.definition = {}
can_access = false this.data = []
t = {} this.resource_class = {}
this.access_msg = ''
this.can_access = false
this.t = {}
}
async vue_on_create() { async vue_on_create() {
this.t = await T( this.t = await T(

View File

@ -8,6 +8,8 @@ import AppSetupComponent from './dash/AppSetup.component.js'
import ListingComponent from './cobalt/Listing.component.js' import ListingComponent from './cobalt/Listing.component.js'
import FormComponent from './cobalt/Form.component.js' import FormComponent from './cobalt/Form.component.js'
import RootPageComponent from './dash/RootPage.component.js'
import OutletComponent from './dash/Outlet.component.js'
import { T } from './service/Translate.service.js' import { T } from './service/Translate.service.js'
@ -22,6 +24,8 @@ const dash_components = {
ListingComponent, ListingComponent,
FormComponent, FormComponent,
RootPageComponent,
OutletComponent,
} }
export { dash_components } export { dash_components }

View File

@ -232,35 +232,39 @@ export default class AppSetupComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return [] } static get props() { return [] }
step = 0 constructor() {
btn_disabled = true super()
btn_back = false
btn_hidden = false
btn_listing = false
name = '' this.step = 0
identifier = '' this.btn_disabled = true
type = '' // ldap | saml | oauth this.btn_back = false
oauth_redirect_uri = '' this.btn_hidden = false
this.btn_listing = false
saml_entity_id = '' this.name = ''
saml_acs_url = '' this.identifier = ''
saml_slo_url = '' this.type = '' // ldap | saml | oauth
this.oauth_redirect_uri = ''
ldap_username = '' this.saml_entity_id = ''
ldap_password = '' this.saml_acs_url = ''
ldap_password_confirm = '' this.saml_slo_url = ''
ldap_config = {}
error_message = '' this.ldap_username = ''
this.ldap_password = ''
this.ldap_password_confirm = ''
this.ldap_config = {}
app = {} this.error_message = ''
oauth_client = {}
saml_provider = {}
ldap_client = {}
app_name = '' this.app = {}
host = '' this.oauth_client = {}
this.saml_provider = {}
this.ldap_client = {}
this.app_name = ''
this.host = ''
}
make_url(path) { make_url(path) {
return session.url(path) return session.url(path)

View File

@ -1,7 +1,7 @@
import { Component } from '../../lib/vues6/vues6.js' import { Component } from '../../lib/vues6/vues6.js'
import { event_bus } from '../service/EventBus.service.js' import { event_bus } from '../service/EventBus.service.js'
import { session } from '../service/Session.service.js' import { session } from '../service/Session.service.js'
import { message_service } from '../service/Message.service.js' import { action_service } from '../service/Action.service.js'
const template = ` const template = `
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom"> <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
@ -36,9 +36,9 @@ const template = `
aria-labelledby="navbarDropdown" aria-labelledby="navbarDropdown"
> >
<h6 class="dropdown-header">Hello, {{ first_name }}.</h6> <h6 class="dropdown-header">Hello, {{ first_name }}.</h6>
<a href="/dash/profile" class="dropdown-item">My Profile</a> <a href="/dash/profile" class="dropdown-item" @click="navigate('dash.profile')" onclick="return false;">My Profile</a>
<a href="/dash/c/listing/reflect/Token" v-if="can.api_tokens" class="dropdown-item">API Tokens</a> <a href="/dash/c/listing/reflect/Token" v-if="can.api_tokens" @click="cobalt('reflect/Token', 'list')" class="dropdown-item" onclick="return false;">API Tokens</a>
<a href="/dash/c/listing/system/Announcement" v-if="can.messages" class="dropdown-item">System Announcements</a> <a href="/dash/c/listing/system/Announcement" v-if="can.messages" @click="cobalt('system/Announcement', 'list')" class="dropdown-item" onclick="return false;">System Announcements</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a href="/auth/logout" class="dropdown-item">Sign-Out of {{ app_name }}</a> <a href="/auth/logout" class="dropdown-item">Sign-Out of {{ app_name }}</a>
</div> </div>
@ -53,10 +53,10 @@ export default class NavBarComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return [] } static get props() { return [] }
can = {}
constructor() { constructor() {
super() super()
this.can = {}
this.toggle_event = event_bus.event('sidebar.toggle') this.toggle_event = event_bus.event('sidebar.toggle')
this.first_name = session.get('user.first_name') this.first_name = session.get('user.first_name')
this.last_name = session.get('user.last_name') this.last_name = session.get('user.last_name')
@ -72,4 +72,20 @@ export default class NavBarComponent extends Component {
toggle_sidebar() { toggle_sidebar() {
this.toggle_event.fire() this.toggle_event.fire()
} }
navigate(page) {
action_service.perform({
action: 'navigate',
page,
})
}
cobalt(resource, action, id = undefined) {
action_service.perform({
type: 'resource',
resource,
action,
id,
})
}
} }

View File

@ -0,0 +1,45 @@
import { Component } from '../../lib/vues6/vues6.js'
import { event_bus } from '../service/EventBus.service.js'
const template = `
<coreid-root :page="page" :form_id="form_id" :resource="resource" :mode="mode" v-if="show"></coreid-root>
`
export default class OutletPageComponent extends Component {
static get selector() { return 'coreid-outlet' }
static get template() { return template }
static get props() { return ['initial_page', 'initial_form_id', 'initial_resource', 'initial_mode'] }
constructor() {
super()
this.navigate_event = event_bus.event('root.navigate')
this.show = true
console.log('navigate event', this.navigate_event)
}
async vue_on_create() {
this.page = this.initial_page
this.form_id = this.initial_form_id
this.resource = this.initial_resource
this.mode = this.initial_mode
this.navigate_event.subscribe((props = {}) => {
console.log('navigation event', props)
this.page = props.page
this.form_id = props.form_id
this.resource = props.resource
this.mode = props.mode
this.rerender()
})
this.$forceUpdate()
}
rerender() {
this.show = false
this.$forceUpdate()
requestAnimationFrame(() => {
this.show = true
this.$forceUpdate()
})
}
}

View File

@ -0,0 +1,32 @@
import { Component } from '../../lib/vues6/vues6.js'
const template = `
<span>
<coreid-profile-edit v-if="page === 'dash.profile'"></coreid-profile-edit>
<coreid-app-setup v-if="page === 'app.setup'"></coreid-app-setup>
<cobalt-form
v-if="page === 'cobalt.form' && form_id"
:resource="resource"
:form_id="form_id"
:initial_mode="mode"
></cobalt-form>
<cobalt-form
v-if="page === 'cobalt.form' && !form_id"
:resource="resource"
:initial_mode="mode"
></cobalt-form>
<cobalt-listing
v-if="page === 'cobalt.listing'"
:resource="resource"
></cobalt-listing>
</span>
`
export default class RootPageComponent extends Component {
static get selector() { return 'coreid-root' }
static get template() { return template }
static get props() { return ['page', 'form_id', 'resource', 'mode'] }
constructor() {
super()
}
}

View File

@ -23,72 +23,99 @@ export default class SideBarComponent extends Component {
static get props() { return ['app_name'] } static get props() { return ['app_name'] }
static get template() { return template } static get template() { return template }
actions = []
possible_actions = [
{
text: 'Profile',
action: 'redirect',
next: '/dash/profile',
},
{
text: 'Users',
action: 'list',
type: 'resource',
resource: 'auth/User',
},
{
text: 'Groups',
action: 'list',
type: 'resource',
resource: 'auth/Group',
},
{
text: 'Applications',
action: 'list',
type: 'resource',
resource: 'App',
},
{
text: 'IAM Policy',
action: 'list',
type: 'resource',
resource: 'iam/Policy',
},
{
text: 'LDAP Clients',
action: 'list',
type: 'resource',
resource: 'ldap/Client',
},
{
text: 'OAuth2 Clients',
action: 'list',
type: 'resource',
resource: 'oauth/Client',
},
{
text: 'OpenID Connect Clients',
action: 'list',
type: 'resource',
resource: 'openid/Client',
},
{
text: 'SAML Service Providers',
action: 'list',
type: 'resource',
resource: 'saml/Provider',
},
{
text: 'Settings',
action: 'list',
type: 'resource',
resource: 'Setting',
},
]
constructor() { constructor() {
super() super()
this.actions = []
this.isCollapsed = false
this.possible_actions = [
{
text: 'Profile',
action: 'navigate',
page: 'dash.profile',
},
{
text: 'Users',
action: 'list',
type: 'resource',
resource: 'auth/User',
},
{
text: 'Groups',
action: 'list',
type: 'resource',
resource: 'auth/Group',
},
{
text: 'Applications',
action: 'list',
type: 'resource',
resource: 'App',
},
{
text: 'IAM Policy',
action: 'list',
type: 'resource',
resource: 'iam/Policy',
},
{
text: 'IAM Permissions',
action: 'list',
type: 'resource',
resource: 'iam/Permission',
},
{
text: 'Computers',
action: 'list',
type: 'resource',
resource: 'ldap/Machine',
},
{
text: 'Computer Groups',
action: 'list',
type: 'resource',
resource: 'ldap/MachineGroup',
},
{
text: 'LDAP Clients',
action: 'list',
type: 'resource',
resource: 'ldap/Client',
},
{
text: 'OAuth2 Clients',
action: 'list',
type: 'resource',
resource: 'oauth/Client',
},
{
text: 'RADIUS Clients',
action: 'list',
type: 'resource',
resource: 'radius/Client',
},
{
text: 'OpenID Connect Clients',
action: 'list',
type: 'resource',
resource: 'openid/Client',
},
{
text: 'SAML Service Providers',
action: 'list',
type: 'resource',
resource: 'saml/Provider',
},
{
text: 'Settings',
action: 'list',
type: 'resource',
resource: 'Setting',
},
]
event_bus.event('sidebar.toggle').subscribe(() => { event_bus.event('sidebar.toggle').subscribe(() => {
this.toggle() this.toggle()
}) })
@ -120,8 +147,6 @@ export default class SideBarComponent extends Component {
this.actions = new_actions this.actions = new_actions
} }
isCollapsed = false
toggle() { toggle() {
this.isCollapsed = !this.isCollapsed this.isCollapsed = !this.isCollapsed
} }

View File

@ -68,8 +68,12 @@ export default class MessageContainerComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return [] } static get props() { return [] }
messages = [] constructor() {
modals = [] super()
this.messages = []
this.modals = []
}
vue_on_create() { vue_on_create() {
this.alert_event = event_bus.event('message.alert') this.alert_event = event_bus.event('message.alert')

View File

@ -8,7 +8,7 @@ import { utility } from '../../service/Utility.service.js'
import { profile_service } from '../../service/Profile.service.js' import { profile_service } from '../../service/Profile.service.js'
const template = ` const template = `
<div class="coreid-profile-container mb-5"> <div class="coreid-profile-container mb-5 offset-0 col-md-8 offset-md-2 col-xl-6 offset-xl-3">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
@ -77,6 +77,20 @@ const template = `
> >
</div> </div>
</div> </div>
<div class="row">
<h4 style="margin-left: 15px">{{ t['profile.advanced_header'] }}</h4>
<div class="col-12 form-group">
<label for="coreid-profile-shell-input">{{ t['profile.advanced_shell'] }}</label>
<input
type="text"
class="form-control"
id="coreid-profile-shell-input"
v-model="profile_shell"
@keyup="on_key_up($event)"
placeholder="/bin/bash"
>
</div>
</div>
</li> </li>
<li class="list-group-item text-right font-italic text-muted"> <li class="list-group-item text-right font-italic text-muted">
{{ form_message }} {{ form_message }}
@ -90,6 +104,11 @@ const template = `
@click="change_password" @click="change_password"
>{{ t['password.change'] }}</button> >{{ t['password.change'] }}</button>
</li> </li>
<li class="list-group-item">
<h4>{{ t['authn.authn'] }}</h4>
<p>{{ t['authn.desc'].replace(/APP_NAME/g, app_name) }}</p>
<button class="btn btn-success btn-sm" type="button">{{ t['authn.enable'] }}</button>
</li>
<li class="list-group-item" v-if="ready && !has_mfa && (!user_id || user_id === 'me')"> <li class="list-group-item" v-if="ready && !has_mfa && (!user_id || user_id === 'me')">
<h4>{{ t['mfa.mfa'] }}</h4> <h4>{{ t['mfa.mfa'] }}</h4>
<p>{{ t['profile.mfa_1'].replace(/APP_NAME/g, app_name) }}</p> <p>{{ t['profile.mfa_1'].replace(/APP_NAME/g, app_name) }}</p>
@ -117,6 +136,7 @@ const template = `
<div class="col-9"> <div class="col-9">
{{ pw.name }} {{ pw.name }}
<br><span class="text-muted font-italic">{{ t['profile.issued'] }} {{ pw.created }}</span> <br><span class="text-muted font-italic">{{ t['profile.issued'] }} {{ pw.created }}</span>
<span class="text-muted font-italic">&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;{{ t['profile.accessed'] }} {{ pw.accessed || t['common.never'] }}</span>
</div> </div>
<div class="col-3 my-auto"> <div class="col-3 my-auto">
<button <button
@ -195,31 +215,36 @@ export default class EditProfileComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return ['user_id'] } static get props() { return ['user_id'] }
profile_first = '' constructor() {
profile_last = '' super()
profile_email = ''
profile_tagline = ''
last_reset = ''
mfa_enable_date = ''
has_mfa_recovery = false this.profile_first = ''
mfa_recovery_date = '' this.profile_last = ''
mfa_recovery_codes = 0 this.profile_email = ''
this.profile_tagline = ''
this.profile_shell = ''
this.last_reset = ''
this.mfa_enable_date = ''
form_message = 'No changes.' this.has_mfa_recovery = false
this.mfa_recovery_date = ''
this.mfa_recovery_codes = 0
has_mfa = false this.form_message = 'No changes.'
ready = false
notify_gateway_url = '' this.has_mfa = false
notify_app_key = '' this.ready = false
notify_enabled = false
notify_created_on = ''
notify_loaded = false
app_passwords = [] this.notify_gateway_url = ''
app_name = '' this.notify_app_key = ''
t = {} this.notify_enabled = false
this.notify_created_on = ''
this.notify_loaded = false
this.app_passwords = []
this.app_name = ''
this.t = {}
}
on_key_up = ($event) => {} on_key_up = ($event) => {}
@ -267,7 +292,14 @@ export default class EditProfileComponent extends Component {
'profile.app_key', 'profile.app_key',
'profile.example_gateway_url', 'profile.example_gateway_url',
'profile.save_notify', 'profile.save_notify',
'profile.test_notify' 'profile.test_notify',
'profile.advanced_header',
'profile.advanced_shell',
'profile.accessed',
'common.never',
'authn.authn',
'authn.desc',
'authn.enable',
) )
this.app_name = session.get('app.name') this.app_name = session.get('app.name')
@ -288,6 +320,7 @@ export default class EditProfileComponent extends Component {
last_name: this.profile_last, last_name: this.profile_last,
email: this.profile_email, email: this.profile_email,
tagline: this.profile_tagline, tagline: this.profile_tagline,
login_shell: this.profile_shell,
user_id: this.user_id || 'me', user_id: this.user_id || 'me',
} }
} }
@ -336,6 +369,7 @@ export default class EditProfileComponent extends Component {
this.profile_last = result.last_name this.profile_last = result.last_name
this.profile_email = result.email this.profile_email = result.email
this.profile_tagline = result.tagline this.profile_tagline = result.tagline
this.profile_shell = result.login_shell
const notify_config = await profile_service.get_notify(this.user_id || 'me') const notify_config = await profile_service.get_notify(this.user_id || 'me')
if ( !notify_config || !notify_config.has_config ) { if ( !notify_config || !notify_config.has_config ) {
@ -380,6 +414,7 @@ export default class EditProfileComponent extends Component {
this.app_passwords = app_pws.map(x => { this.app_passwords = app_pws.map(x => {
if ( x.expires ) x.expires = (new Date(x.expires)).toLocaleDateString() if ( x.expires ) x.expires = (new Date(x.expires)).toLocaleDateString()
if ( x.created ) x.created = (new Date(x.created)).toLocaleDateString() if ( x.created ) x.created = (new Date(x.created)).toLocaleDateString()
if ( x.accessed ) x.accessed = (new Date(x.accessed)).toLocaleDateString()
return x return x
}) })
} }

View File

@ -72,12 +72,16 @@ export default class AppPasswordFormComponent extends Component {
static get template() { return template } static get template() { return template }
static get props() { return [] } static get props() { return [] }
name = '' constructor() {
valid = false super()
uuid = ''
enable_form = true this.name = ''
display_password = '' this.valid = false
t = {} this.uuid = ''
this.enable_form = true
this.display_password = ''
this.t = {}
}
async vue_on_create() { async vue_on_create() {
this.t = await T( this.t = await T(

View File

@ -29,8 +29,12 @@ export default class ProfilePhotoUploaderComponent extends Component {
static get template() { return template } static get template() { return template }
static get params() { return [] } static get params() { return [] }
ready = false constructor() {
t = {} super()
this.ready = false
this.t = {}
}
async vue_on_create() { async vue_on_create() {
this.t = await T( this.t = await T(

View File

@ -2,126 +2,148 @@ import CRUDBase from './CRUDBase.js'
import { session } from '../service/Session.service.js' import { session } from '../service/Session.service.js'
class AppResource extends CRUDBase { class AppResource extends CRUDBase {
endpoint = '/api/v1/applications' constructor() {
required_fields = ['name', 'identifier'] super()
permission_base = 'v1:applications'
item = 'Application' this.endpoint = '/api/v1/applications'
plural = 'Applications' this.required_fields = ['name', 'identifier']
this.permission_base = 'v1:applications'
listing_definition = { this.item = 'Application'
display: ` this.plural = 'Applications'
this.listing_definition = {
display: `
An application is anything that can authenticate users against ${session.get('app.name')}. Applications can have any number of associated LDAP clients, SAML service providers, and OAuth2 clients. An application is anything that can authenticate users against ${session.get('app.name')}. Applications can have any number of associated LDAP clients, SAML service providers, and OAuth2 clients.
`, `,
columns: [ columns: [
{ {
name: 'Name', name: 'Name',
field: 'name', field: 'name',
}, },
{ {
name: 'Identifier', name: 'Identifier',
field: 'identifier', field: 'identifier',
}, },
{ {
name: 'Description', name: 'Description',
field: 'description', field: 'description',
}, },
], ],
actions: [ actions: [
{ {
type: 'resource', type: 'resource',
position: 'main', position: 'main',
action: 'insert', action: 'insert',
text: 'Manual Setup', text: 'Manual Setup',
color: 'outline-success', color: 'outline-success',
}, },
{ {
position: 'main', position: 'main',
action: 'redirect', action: 'navigate',
text: 'Setup Wizard', text: 'Setup Wizard',
color: 'success', color: 'success',
next: '/dash/app/setup', page: 'app.setup',
}, },
{ {
type: 'resource', type: 'resource',
position: 'row', position: 'row',
action: 'update', action: 'update',
icon: 'fa fa-edit', icon: 'fa fa-edit',
color: 'primary', color: 'primary',
}, },
{ {
type: 'resource', type: 'resource',
position: 'row', position: 'row',
action: 'delete', action: 'delete',
icon: 'fa fa-times', icon: 'fa fa-times',
color: 'danger', color: 'danger',
confirm: true, confirm: true,
}, },
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Name', name: 'Name',
field: 'name', field: 'name',
placeholder: 'Awesome App', placeholder: 'Awesome App',
required: true, required: true,
type: 'text', type: 'text',
},
{
name: 'Identifier',
field: 'identifier',
placeholder: 'awesome_app',
required: true,
type: 'text',
},
{
name: 'Description',
field: 'description',
type: 'textarea',
},
{
name: 'Associated LDAP Clients',
field: 'ldap_client_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'ldap/Client',
display: 'name',
value: 'id',
}, },
}, {
{ name: 'Identifier',
name: 'Associated OAuth2 Clients', field: 'identifier',
field: 'oauth_client_ids', placeholder: 'awesome_app',
type: 'select.dynamic.multiple', required: true,
options: { type: 'text',
resource: 'oauth/Client',
display: 'name',
value: 'id',
}, },
}, {
{ name: 'Description',
name: 'Associated OpenID Connect Clients', field: 'description',
field: 'openid_client_ids', type: 'textarea',
type: 'select.dynamic.multiple',
options: {
resource: 'openid/Client',
display: 'client_name',
value: 'id',
}, },
}, {
{ name: 'IAM Target',
name: 'Associated SAML Service Providers', field: 'id',
field: 'saml_service_provider_ids', type: 'text',
type: 'select.dynamic.multiple', readonly: true,
options: { hidden: ['insert'],
resource: 'saml/Provider', help: `(LDAP use) Allows restricting users to only those that can access this application. (filter: iamTarget)`,
display: 'name',
value: 'id',
}, },
}, {
], name: 'Associated LDAP Clients',
field: 'ldap_client_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'ldap/Client',
display: 'name',
value: 'id',
},
},
{
name: 'Associated OAuth2 Clients',
field: 'oauth_client_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'oauth/Client',
display: 'name',
value: 'id',
},
},
{
name: 'Associated RADIUS Clients',
field: 'radius_client_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'radius/Client',
display: 'name',
value: 'id',
},
},
{
name: 'Associated OpenID Connect Clients',
field: 'openid_client_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'openid/Client',
display: 'client_name',
value: 'id',
},
},
{
name: 'Associated SAML Service Providers',
field: 'saml_service_provider_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'saml/Provider',
display: 'name',
value: 'id',
},
},
],
}
} }
} }

View File

@ -2,15 +2,17 @@ import APIParseError from './APIParseError.js'
import { session } from '../service/Session.service.js' import { session } from '../service/Session.service.js'
export default class CRUDBase { export default class CRUDBase {
endpoint = '/api/v1' constructor() {
required_fields = [] this.endpoint = '/api/v1'
permission_base = '' this.required_fields = []
this.permission_base = ''
listing_definition = {} this.listing_definition = {}
form_definition = {} this.form_definition = {}
item = '' this.item = ''
plural = '' this.plural = ''
}
async can(action) { async can(action) {
return session.check_permissions(`${this.permission_base}:${action}`) return session.check_permissions(`${this.permission_base}:${action}`)

View File

@ -2,53 +2,57 @@ import CRUDBase from './CRUDBase.js'
import { session } from '../service/Session.service.js' import { session } from '../service/Session.service.js'
class SettingResource extends CRUDBase { class SettingResource extends CRUDBase {
endpoint = '/api/v1/settings' constructor() {
required_fields = ['key', 'value'] super()
permission_base = 'v1:settings'
item = 'Setting' this.endpoint = '/api/v1/settings'
plural = 'Settings' this.required_fields = ['key', 'value']
this.permission_base = 'v1:settings'
listing_definition = { this.item = 'Setting'
display: ` this.plural = 'Settings'
this.listing_definition = {
display: `
<p>These are advanced settings that allow you to tweak the way ${session.get('app.name')} behaves. Tweak them at your own risk.</p> <p>These are advanced settings that allow you to tweak the way ${session.get('app.name')} behaves. Tweak them at your own risk.</p>
`, `,
columns: [ columns: [
{ {
name: 'Setting Key', name: 'Setting Key',
field: 'key', field: 'key',
}, },
{ {
name: 'Value', name: 'Value',
field: 'value', field: 'value',
renderer: (v) => JSON.stringify(v), renderer: (v) => JSON.stringify(v),
}, },
], ],
actions: [ actions: [
{ {
type: 'resource', type: 'resource',
position: 'row', position: 'row',
action: 'update', action: 'update',
icon: 'fa fa-edit', icon: 'fa fa-edit',
color: 'primary', color: 'primary',
}, },
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Setting Key', name: 'Setting Key',
field: 'key', field: 'key',
type: 'text', type: 'text',
readonly: true, readonly: true,
}, },
{ {
name: 'Value (JSON)', name: 'Value (JSON)',
field: 'value', field: 'value',
type: 'json', type: 'json',
}, },
], ],
}
} }
} }

View File

@ -2,74 +2,87 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class GroupResource extends CRUDBase { class GroupResource extends CRUDBase {
endpoint = '/api/v1/auth/groups' constructor() {
required_fields = ['name'] super()
permission_base = 'v1:auth:groups'
item = 'Group' this.endpoint = '/api/v1/auth/groups'
plural = 'Groups' this.required_fields = ['name']
this.permission_base = 'v1:auth:groups'
listing_definition = { this.item = 'Group'
display: ` this.plural = 'Groups'
In ${session.get('app.name')}, groups are simply a tool for organizing users and assigning permissions and access in bulk. After creating and assigning users to a group, you can manage permissions for that group, and its policies will be applied to all users in that group.
`,
columns: [
{
name: 'Name',
field: 'name',
},
{
name: '# of Users',
field: 'user_ids',
renderer: (user_ids) => Array.isArray(user_ids) ? user_ids.length : 0,
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
form_definition = { this.listing_definition = {
fields: [ display: `
{ In ${session.get('app.name')}, groups are simply a tool for organizing users and assigning permissions and access in bulk. After creating and assigning users to a group, you can manage permissions for that group, and its policies will be applied to all users in that group.
name: 'Name', `,
field: 'name', columns: [
placeholder: 'Some Cool Users', {
required: true, name: 'Name',
type: 'text', field: 'name',
},
{
name: 'Users',
field: 'user_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'auth/User',
display: (user) => `${user.last_name}, ${user.first_name} (${user.uid})`,
value: 'id',
}, },
}, {
], name: '# of Users',
field: 'user_ids',
renderer: (user_ids) => Array.isArray(user_ids) ? user_ids.length : 0,
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
this.form_definition = {
fields: [
{
name: 'Name',
field: 'name',
placeholder: 'Some Cool Users',
required: true,
type: 'text',
},
/*{
name: 'Superuser equivalent?',
field: 'grants_sudo',
type: 'select',
options: [
{display: 'Yes', value: true},
{display: 'No', value: false},
],
},*/
{
name: 'Users',
field: 'user_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'auth/User',
display: (user) => `${user.last_name}, ${user.first_name} (${user.uid})`,
value: 'id',
},
},
],
}
} }
} }

View File

@ -1,12 +1,17 @@
import CRUDBase from '../CRUDBase.js' import CRUDBase from '../CRUDBase.js'
class RoleResource extends CRUDBase { class RoleResource extends CRUDBase {
endpoint = '/api/v1/auth/roles'
required_fields = ['role', 'permissions']
permission_base = 'v1:auth:roles'
item = 'Role' constructor() {
plural = 'Roles' super()
this.endpoint = '/api/v1/auth/roles'
this.required_fields = ['role', 'permissions']
this.permission_base = 'v1:auth:roles'
this.item = 'Role'
this.plural = 'Roles'
}
} }
const auth_role = new RoleResource() const auth_role = new RoleResource()

View File

@ -1,12 +1,16 @@
import CRUDBase from '../CRUDBase.js' import CRUDBase from '../CRUDBase.js'
class TrapResource extends CRUDBase { class TrapResource extends CRUDBase {
endpoint = '/api/v1/auth/traps' constructor() {
required_fields = ['name', 'trap', 'redirect_to'] super()
permission_base = 'v1:auth:traps'
item = 'Trap' this.endpoint = '/api/v1/auth/traps'
plural = 'Traps' this.required_fields = ['name', 'trap', 'redirect_to']
this.permission_base = 'v1:auth:traps'
this.item = 'Trap'
this.plural = 'Traps'
}
} }
const auth_trap = new TrapResource() const auth_trap = new TrapResource()

View File

@ -2,114 +2,118 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class UserResource extends CRUDBase { class UserResource extends CRUDBase {
endpoint = '/api/v1/auth/users' constructor() {
required_fields = ['uid', 'first_name', 'last_name', 'email'] super()
permission_base = 'v1:auth:users'
item = 'User' this.endpoint = '/api/v1/auth/users'
plural = 'Users' this.required_fields = ['uid', 'first_name', 'last_name', 'email']
this.permission_base = 'v1:auth:users'
listing_definition = { this.item = 'User'
display: ` this.plural = 'Users'
this.listing_definition = {
display: `
Users can be assigned permissions and, if granted, can manage their ${session.get('app.name')} accounts from the Profile page, as well as login to the external applications they've been given access to. Users can be assigned permissions and, if granted, can manage their ${session.get('app.name')} accounts from the Profile page, as well as login to the external applications they've been given access to.
`, `,
columns: [ columns: [
{ {
name: 'UID', name: 'UID',
field: 'uid', field: 'uid',
},
{
name: 'Last Name',
field: 'last_name',
},
{
name: 'First Name',
field: 'first_name',
},
{
name: 'E-Mail',
field: 'email',
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
form_definition = {
fields: [
{
name: 'First Name',
field: 'first_name',
placeholder: 'John',
required: true,
type: 'text',
},
{
name: 'Last Name',
field: 'last_name',
placeholder: 'Doe',
required: true,
type: 'text',
},
{
name: 'Username',
field: 'uid',
placeholder: 'john.doe',
required: true,
type: 'text',
},
{
name: 'E-Mail',
field: 'email',
placeholder: 'john@contoso.com',
required: true,
type: 'text',
},
{
name: 'Tagline',
field: 'tagline',
type: 'text',
},
{
name: 'Password',
field: 'password',
type: 'password',
placeholder: 'Password',
required: ['insert'],
},
{
name: 'Trap',
field: 'trap',
type: 'select.dynamic',
options: {
resource: 'auth/Trap',
display: 'name',
value: 'trap',
}, },
}, {
], name: 'Last Name',
field: 'last_name',
},
{
name: 'First Name',
field: 'first_name',
},
{
name: 'E-Mail',
field: 'email',
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
this.form_definition = {
fields: [
{
name: 'First Name',
field: 'first_name',
placeholder: 'John',
required: true,
type: 'text',
},
{
name: 'Last Name',
field: 'last_name',
placeholder: 'Doe',
required: true,
type: 'text',
},
{
name: 'Username',
field: 'uid',
placeholder: 'john.doe',
required: true,
type: 'text',
},
{
name: 'E-Mail',
field: 'email',
placeholder: 'john@contoso.com',
required: true,
type: 'text',
},
{
name: 'Tagline',
field: 'tagline',
type: 'text',
},
{
name: 'Password',
field: 'password',
type: 'password',
placeholder: 'Password',
required: ['insert'],
},
{
name: 'Trap',
field: 'trap',
type: 'select.dynamic',
options: {
resource: 'auth/Trap',
display: 'name',
value: 'trap',
},
},
],
}
} }
} }

View File

@ -0,0 +1,87 @@
import CRUDBase from '../CRUDBase.js'
class PermissionResource extends CRUDBase {
constructor() {
super()
this.endpoint = '/api/v1/iam/permission'
this.required_fields = ['target_type', 'permission']
this.permission_base = 'v1:iam:permission'
this.item = 'IAM Permission'
this.plural = 'IAM Permissions'
this.listing_definition = {
display: `Permissions are custom actions that can be performed on a given IAM target by the subject.`,
columns: [
{
name: 'Target Type',
field: 'target_type',
renderer: type => type.split('_').map(x => `${x.charAt(0).toUpperCase()}${x.slice(1)}`).join(' '),
},
{
name: 'Permission',
field: 'permission',
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
this.form_definition = {
fields: [
{
name: 'Target Type',
field: 'target_type',
required: true,
type: 'select',
options: [
{display: 'Application', value: 'application'},
{display: 'Api Scope', value: 'api_scope'},
{display: 'Machine', value: 'machine'},
{display: 'Machine Group', value: 'machine_group'},
],
},
{
name: 'Permission',
field: 'permission',
required: true,
type: 'text',
},
],
/*handlers: {
insert: {
action: 'back',
},
update: {
action: 'back',
},
},*/
}
}
}
const iam_permission = new PermissionResource()
export { iam_permission }

View File

@ -2,15 +2,18 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class PolicyResource extends CRUDBase { class PolicyResource extends CRUDBase {
endpoint = '/api/v1/iam/policy' constructor() {
required_fields = ['entity_id', 'entity_type', 'target_id', 'target_type', 'access_type'] super()
permission_base = 'v1:iam:policy'
item = 'IAM Policy' this.endpoint = '/api/v1/iam/policy'
plural = 'IAM Policies' this.required_fields = ['entity_id', 'entity_type', 'target_id', 'target_type', 'access_type']
this.permission_base = 'v1:iam:policy'
listing_definition = { this.item = 'IAM Policy'
display: ` this.plural = 'IAM Policies'
this.listing_definition = {
display: `
Identity & Access Management (IAM) policies give you fine grained control over which ${session.get('app.name')} users and groups are allowed to access which applications. Identity & Access Management (IAM) policies give you fine grained control over which ${session.get('app.name')} users and groups are allowed to access which applications.
<br><br> <br><br>
An IAM policy has three parts. First, is the subject. The subject is who the policy applies to and is either a user or a group. The second part is the access type. This is either an allowance or a denial. That is, the policy either grants a subject access to a resource, or explicitly denies them access. The final part of the policy is the target. This is the application that the subject is being granted or denied access to. An IAM policy has three parts. First, is the subject. The subject is who the policy applies to and is either a user or a group. The second part is the access type. This is either an allowance or a denial. That is, the policy either grants a subject access to a resource, or explicitly denies them access. The final part of the policy is the target. This is the application that the subject is being granted or denied access to.
@ -24,136 +27,232 @@ class PolicyResource extends CRUDBase {
</ol> </ol>
This means, for example, that if a user's group is allowed access, but a user is denied access, the user will be denied access. Likewise, if there are two policies for a subject, one granting them access and one denying them access, the denial will take precedence. This means, for example, that if a user's group is allowed access, but a user is denied access, the user will be denied access. Likewise, if there are two policies for a subject, one granting them access and one denying them access, the denial will take precedence.
`, `,
columns: [ columns: [
{ {
name: 'Subject', name: 'Subject',
field: 'entity_display', field: 'entity_display',
}, },
{ {
name: 'Access Type', name: 'Access Type',
field: 'access_type', field: 'access_type',
renderer: access_type => access_type === 'deny' ? '...is denied access to...' : '...is granted access to...', renderer: access_type => access_type === 'deny' ? '...is denied access to...' : '...is granted access to...',
}, },
{ {
name: 'Target', name: 'Target',
field: 'target_display', field: 'target_display',
}, },
], {
actions: [ name: 'Permission',
{ field: 'permission',
type: 'resource', renderer: permission => permission || '-',
position: 'main', },
action: 'insert', ],
text: 'Create New', actions: [
color: 'success', {
}, type: 'resource',
{ position: 'main',
type: 'resource', action: 'insert',
position: 'row', text: 'Create New',
action: 'update', color: 'success',
icon: 'fa fa-edit', },
color: 'primary', {
}, type: 'resource',
{ position: 'row',
type: 'resource', action: 'update',
position: 'row', icon: 'fa fa-edit',
action: 'delete', color: 'primary',
icon: 'fa fa-times', },
color: 'danger', {
confirm: true, type: 'resource',
}, position: 'row',
], action: 'delete',
} icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Subject Type', name: 'Subject Type',
field: 'entity_type', field: 'entity_type',
required: true, required: true,
type: 'select', type: 'select',
options: [ options: [
{ display: 'User', value: 'user' }, {display: 'User', value: 'user'},
{ display: 'Group', value: 'group' }, {display: 'Group', value: 'group'},
], ],
},
{
name: 'Subject',
field: 'entity_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'auth/User',
display: user => `User: ${user.last_name}, ${user.first_name} (${user.uid})`,
value: 'id',
}, },
if: (form_data) => form_data.entity_type === 'user', {
}, name: 'Subject',
{ field: 'entity_id',
name: 'Subject', required: true,
field: 'entity_id', type: 'select.dynamic',
required: true, options: {
type: 'select.dynamic', resource: 'auth/User',
options: { display: user => `User: ${user.last_name}, ${user.first_name} (${user.uid})`,
resource: 'auth/Group', value: 'id',
display: group => `Group: ${group.name} (${group.user_ids.length} users)`, },
value: 'id', if: (form_data) => form_data.entity_type === 'user',
}, },
if: (form_data) => form_data.entity_type === 'group', {
}, name: 'Subject',
{ field: 'entity_id',
name: 'Access Type', required: true,
field: 'access_type', type: 'select.dynamic',
required: true, options: {
type: 'select', resource: 'auth/Group',
options: [ display: group => `Group: ${group.name} (${group.user_ids.length} users)`,
{ display: '...is granted access to...', value: 'allow' }, value: 'id',
{ display: '...is denied access to...', value: 'deny' }, },
], if: (form_data) => form_data.entity_type === 'group',
},
{
name: 'Target Type',
field: 'target_type',
required: true,
type: 'select',
options: [
{ display: 'Application', value: 'application' },
{ display: 'API Scope', value: 'api_scope' },
],
},
{
name: 'Target',
field: 'target_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'App',
display: 'name',
value: 'id',
}, },
if: (form_data) => form_data.target_type === 'application' {
}, name: 'Access Type',
{ field: 'access_type',
name: 'Target', required: true,
field: 'target_id', type: 'select',
required: true, options: [
type: 'select.dynamic', {display: '...is granted access to...', value: 'allow'},
options: { {display: '...is denied access to...', value: 'deny'},
resource: 'reflect/Scope', ],
display: 'scope',
value: 'scope',
}, },
if: (form_data) => form_data.target_type === 'api_scope' {
}, name: 'Target Type',
], field: 'target_type',
/*handlers: { required: true,
insert: { type: 'select',
action: 'back', options: [
}, {display: 'Application', value: 'application'},
update: { {display: 'API Scope', value: 'api_scope'},
action: 'back', {display: 'Computer', value: 'machine'},
}, {display: 'Computer Group', value: 'machine_group'},
},*/ ],
},
{
name: 'Target',
field: 'target_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'App',
display: 'name',
value: 'id',
},
if: (form_data) => form_data.target_type === 'application'
},
{
name: 'Target',
field: 'target_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'reflect/Scope',
display: 'scope',
value: 'scope',
},
if: (form_data) => form_data.target_type === 'api_scope'
},
{
name: 'Target',
field: 'target_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'ldap/Machine',
display: machine => `${machine.name}${machine.host_name ? ' (' + machine.host_name + ')' : ''}`,
value: 'id',
},
if: (form_data) => form_data.target_type === 'machine'
},
{
name: 'Target',
field: 'target_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'ldap/MachineGroup',
display: group => `${group.name} (${(group.machine_ids || []).length} computers)`,
value: 'id',
},
if: (form_data) => form_data.target_type === 'machine_group'
},
{
name: 'Permission',
field: 'permission',
required: false,
type: 'select.dynamic',
options: {
resource: 'iam/Permission',
display: 'permission',
value: 'permission',
other_params: {
target_type: 'application',
include_unset: true,
},
},
if: (form_data, opts) => form_data.target_type === 'application' && opts?.length
},
{
name: 'Permission',
field: 'permission',
required: false,
type: 'select.dynamic',
options: {
resource: 'iam/Permission',
display: 'permission',
value: 'permission',
other_params: {
target_type: 'api_scope',
include_unset: true,
},
},
if: (form_data, opts) => form_data.target_type === 'api_scope' && opts?.length
},
{
name: 'Permission',
field: 'permission',
required: false,
type: 'select.dynamic',
options: {
resource: 'iam/Permission',
display: 'permission',
value: 'permission',
other_params: {
target_type: 'machine',
include_unset: true,
},
},
if: (form_data, opts) => form_data.target_type === 'machine' && opts?.length
},
{
name: 'Permission',
field: 'permission',
required: false,
type: 'select.dynamic',
options: {
resource: 'iam/Permission',
display: 'permission',
value: 'permission',
other_params: {
target_type: 'machine_group',
include_unset: true,
},
},
if: (form_data, opts) => form_data.target_type === 'machine_group' && opts?.length
},
],
/*handlers: {
insert: {
action: 'back',
},
update: {
action: 'back',
},
},*/
}
} }
} }

View File

@ -2,83 +2,88 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class ClientResource extends CRUDBase { class ClientResource extends CRUDBase {
endpoint = '/api/v1/ldap/clients' constructor() {
required_fields = ['name', 'uid', 'password'] super()
permission_base = 'v1:ldap:clients'
item = 'LDAP Client' this.endpoint = '/api/v1/ldap/clients'
plural = 'LDAP Clients' this.required_fields = ['name', 'uid', 'password']
this.permission_base = 'v1:ldap:clients'
async server_config() { this.item = 'LDAP Client'
const results = await axios.get('/api/v1/ldap/config') this.plural = 'LDAP Clients'
if ( results && results.data && results.data.data ) return results.data.data
}
listing_definition = {
display: ` this.listing_definition = {
display: `
LDAP Clients are special user accounts that external applications can use to bind to ${session.get('app.name')}'s built-in LDAP server to allow these applications to authenticate users. LDAP Clients are special user accounts that external applications can use to bind to ${session.get('app.name')}'s built-in LDAP server to allow these applications to authenticate users.
<br><br> <br><br>
These special accounts are permitted to bind to the LDAP server, but are not allowed to sign-in to ${session.get('app.name')}. These special accounts are permitted to bind to the LDAP server, but are not allowed to sign-in to ${session.get('app.name')}.
`, `,
columns: [ columns: [
{ {
name: 'Client Name', name: 'Client Name',
field: 'name', field: 'name',
}, },
{ {
name: 'User ID', name: 'User ID',
field: 'uid', field: 'uid',
}, },
], ],
actions: [ actions: [
{ {
type: 'resource', type: 'resource',
position: 'main', position: 'main',
action: 'insert', action: 'insert',
text: 'Create New', text: 'Create New',
color: 'success', color: 'success',
}, },
{ {
type: 'resource', type: 'resource',
position: 'row', position: 'row',
action: 'update', action: 'update',
icon: 'fa fa-edit', icon: 'fa fa-edit',
color: 'primary', color: 'primary',
}, },
{ {
type: 'resource', type: 'resource',
position: 'row', position: 'row',
action: 'delete', action: 'delete',
icon: 'fa fa-times', icon: 'fa fa-times',
color: 'danger', color: 'danger',
confirm: true, confirm: true,
}, },
], ],
}
this.form_definition = {
fields: [
{
name: 'Provider Name',
field: 'name',
placeholder: 'Awesome External App',
required: true,
type: 'text',
},
{
name: 'User ID',
field: 'uid',
placeholder: 'some_username',
required: true,
type: 'text',
},
{
name: 'Password',
field: 'password',
required: ['insert'],
type: 'password',
},
],
}
} }
form_definition = { async server_config() {
fields: [ const results = await axios.get('/api/v1/ldap/config')
{ if (results && results.data && results.data.data) return results.data.data
name: 'Provider Name',
field: 'name',
placeholder: 'Awesome External App',
required: true,
type: 'text',
},
{
name: 'User ID',
field: 'uid',
placeholder: 'some_username',
required: true,
type: 'text',
},
{
name: 'Password',
field: 'password',
required: ['insert'],
type: 'password',
},
],
} }
} }

View File

@ -1,97 +1,101 @@
import CRUDBase from '../CRUDBase.js' import CRUDBase from '../CRUDBase.js'
class GroupResource extends CRUDBase { class GroupResource extends CRUDBase {
endpoint = '/api/v1/ldap/groups' constructor() {
required_fields = ['name', 'role'] super()
permission_base = 'v1:ldap:groups'
item = 'LDAP Group' this.endpoint = '/api/v1/ldap/groups'
plural = 'LDAP Groups' this.required_fields = ['name', 'role']
this.permission_base = 'v1:ldap:groups'
listing_definition = { this.item = 'LDAP Group'
columns: [ this.plural = 'LDAP Groups'
{
name: 'Group Name',
field: 'name',
},
{
name: 'Role',
field: 'role',
},
{
name: '# of Users',
field: 'user_ids',
renderer: (user_ids) => Array.isArray(user_ids) ? user_ids.length : 0,
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
form_definition = { this.listing_definition = {
// back_action: { columns: [
// text: 'Back', {
// action: 'back', name: 'Group Name',
// }, field: 'name',
fields: [
{
name: 'Group Name',
field: 'name',
placeholder: 'External App Users',
required: true,
type: 'text',
},
{
name: 'Role',
field: 'role',
placeholder: 'external_app',
required: true,
type: 'select.dynamic',
options: {
resource: 'auth/Role',
display: 'role',
value: 'role',
}, },
// options: [ {
// { value: 1, display: 'One' }, name: 'Role',
// { value: 2, display: 'Two' }, field: 'role',
// { value: 3, display: 'Three' },
// ],
},
{
name: 'Users',
field: 'user_ids',
placeholder: 'John Doe',
type: 'select.dynamic.multiple',
options: {
resource: 'auth/User',
display: (user) => `${user.last_name}, ${user.first_name} (${user.uid})`,
value: 'id',
}, },
}, {
], name: '# of Users',
field: 'user_ids',
renderer: (user_ids) => Array.isArray(user_ids) ? user_ids.length : 0,
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
this.form_definition = {
// back_action: {
// text: 'Back',
// action: 'back',
// },
fields: [
{
name: 'Group Name',
field: 'name',
placeholder: 'External App Users',
required: true,
type: 'text',
},
{
name: 'Role',
field: 'role',
placeholder: 'external_app',
required: true,
type: 'select.dynamic',
options: {
resource: 'auth/Role',
display: 'role',
value: 'role',
},
// options: [
// { value: 1, display: 'One' },
// { value: 2, display: 'Two' },
// { value: 3, display: 'Three' },
// ],
},
{
name: 'Users',
field: 'user_ids',
placeholder: 'John Doe',
type: 'select.dynamic.multiple',
options: {
resource: 'auth/User',
display: (user) => `${user.last_name}, ${user.first_name} (${user.uid})`,
value: 'id',
},
},
],
}
} }
} }

View File

@ -0,0 +1,108 @@
import CRUDBase from '../CRUDBase.js'
class MachineResource extends CRUDBase {
constructor() {
super()
this.endpoint = '/api/v1/ldap/machines'
this.required_fields = ['name', 'description']
this.permission_base = 'v1:ldap:machines'
this.item = 'Computer'
this.plural = 'Computers'
this.listing_definition = {
columns: [
{
name: 'Machine Name',
field: 'name',
},
{
name: 'Host Name',
field: 'host_name',
},
{
name: 'Description',
field: 'description',
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
this.form_definition = {
// back_action: {
// text: 'Back',
// action: 'back',
// },
fields: [
{
name: 'Machine Name',
field: 'name',
placeholder: 'DNS01',
required: true,
type: 'text',
},
{
name: 'Description',
field: 'description',
required: true,
type: 'textarea',
},
{
name: 'Location',
field: 'location',
type: 'text',
placeholder: 'Server room 1',
},
{
name: 'Host Name (FQDN)',
field: 'host_name',
type: 'text',
placeholder: 'dns01.my.domain',
},
{
name: 'IAM Target',
field: 'id',
type: 'text',
readonly: true,
hidden: ['insert'],
help: `(LDAP use) Allows restricting users to only those that can access this computer. (filter: iamTarget)`,
},
{
name: 'IAM Filter',
field: 'iam_filter',
type: 'text',
readonly: true,
hidden: ['insert'],
help: `(LDAP use) Use this filter to restrict access to only users granted IAM access to this computer.`,
},
],
}
}
}
const ldap_machine = new MachineResource()
export { ldap_machine }

View File

@ -0,0 +1,98 @@
import CRUDBase from '../CRUDBase.js'
class MachineGroupResource extends CRUDBase {
constructor() {
super()
this.endpoint = '/api/v1/ldap/machine-groups'
this.required_fields = ['name']
this.permission_base = 'v1:ldap:machine_groups'
this.item = 'Computer Group'
this.plural = 'Computer Groups'
this.listing_definition = {
columns: [
{
name: 'Group Name',
field: 'name',
},
{
name: '# Computers',
field: 'machine_ids',
renderer: machine_ids => Array.isArray(machine_ids) ? machine_ids.length : 0,
},
{
name: 'Description',
field: 'description',
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
this.form_definition = {
// back_action: {
// text: 'Back',
// action: 'back',
// },
fields: [
{
name: 'Group Name',
field: 'name',
placeholder: 'DNS Servers',
required: true,
type: 'text',
},
{
name: 'Description',
field: 'description',
type: 'textarea',
},
{
name: 'IAM Target',
field: 'id',
type: 'text',
readonly: true,
hidden: ['insert'],
help: `(LDAP use) Allows restricting users to only those that can access this computer group. (filter: iamTarget)`,
},
{
name: 'Computers',
field: 'machine_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'ldap/Machine',
display: machine => `${machine.name}${machine.host_name ? ' (' + machine.host_name + ')' : ''}`,
value: 'id',
},
},
],
}
}
}
const ldap_machinegroup = new MachineGroupResource()
export { ldap_machinegroup }

View File

@ -2,104 +2,108 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js'; import { session } from '../../service/Session.service.js';
class ClientResource extends CRUDBase { class ClientResource extends CRUDBase {
endpoint = '/api/v1/oauth/clients' constructor() {
required_fields = ['name', 'redirect_url', 'api_scopes'] super()
permission_base = 'v1:oauth:clients'
item = 'OAuth2 Client' this.endpoint = '/api/v1/oauth/clients'
plural = 'OAuth2 Clients' this.required_fields = ['name', 'redirect_url', 'api_scopes']
this.permission_base = 'v1:oauth:clients'
listing_definition = { this.item = 'OAuth2 Client'
display: ` this.plural = 'OAuth2 Clients'
this.listing_definition = {
display: `
OAuth2 clients are applications that support authentication over the OAuth2 protocol. This allows you to add a "Sign-In with XXX" button for ${session.get('app.name')} to the application in question. To do this, you need to create an OAuth2 client for that application, and provide the name, redirect URL, and API scopes. OAuth2 clients are applications that support authentication over the OAuth2 protocol. This allows you to add a "Sign-In with XXX" button for ${session.get('app.name')} to the application in question. To do this, you need to create an OAuth2 client for that application, and provide the name, redirect URL, and API scopes.
<br><br> <br><br>
You must select the API scopes to grant this OAuth2 client. This defines what ${session.get('app.name')} endpoints the application is allowed to access. For most applications, granting the <code>v1:api:users:get</code> and <code>v1:api:groups:get</code> API scopes should be sufficient. You must select the API scopes to grant this OAuth2 client. This defines what ${session.get('app.name')} endpoints the application is allowed to access. For most applications, granting the <code>v1:api:users:get</code> and <code>v1:api:groups:get</code> API scopes should be sufficient.
<br><br> <br><br>
This method can also be used to access the API for other purposes. Hence, the expansive API scopes. ${session.get('app.name')} uses Flitter-Auth's built-in OAuth2 server under the hood, so you can find details on how to configure the OAuth2 clients <a href="https://flitter.garrettmills.dev/tutorial-flitter-auth-oauth2-server.html" target="_blank">here.</a> This method can also be used to access the API for other purposes. Hence, the expansive API scopes. ${session.get('app.name')} uses Flitter-Auth's built-in OAuth2 server under the hood, so you can find details on how to configure the OAuth2 clients <a href="https://flitter.garrettmills.dev/tutorial-flitter-auth-oauth2-server.html" target="_blank">here.</a>
`, `,
columns: [ columns: [
{ {
name: 'Client Name', name: 'Client Name',
field: 'name', field: 'name',
},
{
name: '# of Scopes',
field: 'api_scopes',
renderer: (api_scopes) => api_scopes.length,
},
{
name: 'Redirect URL',
field: 'redirect_url',
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
form_definition = {
fields: [
{
name: 'Client Name',
field: 'name',
placeholder: 'Awesome External App',
required: true,
type: 'text',
},
{
name: 'Redirect URL',
field: 'redirect_url',
placeholder: 'https://awesome.app/oauth2/callback',
required: true,
type: 'text',
},
{
name: 'API Scopes',
field: 'api_scopes',
type: 'select.dynamic.multiple',
options: {
resource: 'reflect/Scope',
display: 'scope',
value: 'scope',
}, },
required: true, {
}, name: '# of Scopes',
{ field: 'api_scopes',
name: 'Client ID', renderer: (api_scopes) => api_scopes.length,
field: 'uuid', },
type: 'text', {
readonly: true, name: 'Redirect URL',
hidden: ['insert'], field: 'redirect_url',
}, },
{ ],
name: 'Client Secret', actions: [
field: 'secret', {
type: 'text', type: 'resource',
readonly: true, position: 'main',
hidden: ['insert'], action: 'insert',
}, text: 'Create New',
], color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
this.form_definition = {
fields: [
{
name: 'Client Name',
field: 'name',
placeholder: 'Awesome External App',
required: true,
type: 'text',
},
{
name: 'Redirect URL',
field: 'redirect_url',
placeholder: 'https://awesome.app/oauth2/callback',
required: true,
type: 'text',
},
{
name: 'API Scopes',
field: 'api_scopes',
type: 'select.dynamic.multiple',
options: {
resource: 'reflect/Scope',
display: 'scope',
value: 'scope',
},
required: true,
},
{
name: 'Client ID',
field: 'uuid',
type: 'text',
readonly: true,
hidden: ['insert'],
},
{
name: 'Client Secret',
field: 'secret',
type: 'text',
readonly: true,
hidden: ['insert'],
},
],
}
} }
} }

View File

@ -2,94 +2,98 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class ClientResource extends CRUDBase { class ClientResource extends CRUDBase {
endpoint = '/openid/clients' constructor() {
required_fields = ['client_name', 'grant_types', 'redirect_uri'] super()
permission_base = 'v1:openid:clients'
item = 'OpenID Connect Client' this.endpoint = '/openid/clients'
plural = 'OpenID Connect Clients' this.required_fields = ['client_name', 'grant_types', 'redirect_uri']
this.permission_base = 'v1:openid:clients'
listing_definition = { this.item = 'OpenID Connect Client'
display: ` this.plural = 'OpenID Connect Clients'
this.listing_definition = {
display: `
OpenID Connect clients are applications that support authentication over the OpenID Connect protocol. This allows you to add a "Sign-In with XXX" button for ${session.get('app.name')} to the application in question. To do this, the application need only comply with the OpenID standards. OpenID Connect clients are applications that support authentication over the OpenID Connect protocol. This allows you to add a "Sign-In with XXX" button for ${session.get('app.name')} to the application in question. To do this, the application need only comply with the OpenID standards.
`, `,
columns: [ columns: [
{ {
name: 'Client Name', name: 'Client Name',
field: 'client_name', field: 'client_name',
}, },
{ {
name: 'Redirect URI', name: 'Redirect URI',
field: 'redirect_uri', field: 'redirect_uri',
}, },
], ],
actions: [ actions: [
{ {
type: 'resource', type: 'resource',
position: 'main', position: 'main',
action: 'insert', action: 'insert',
text: 'Create New', text: 'Create New',
color: 'success', color: 'success',
}, },
{ {
type: 'resource', type: 'resource',
position: 'row', position: 'row',
action: 'update', action: 'update',
icon: 'fa fa-edit', icon: 'fa fa-edit',
color: 'primary', color: 'primary',
}, },
{ {
type: 'resource', type: 'resource',
position: 'row', position: 'row',
action: 'delete', action: 'delete',
icon: 'fa fa-times', icon: 'fa fa-times',
color: 'danger', color: 'danger',
confirm: true, confirm: true,
}, },
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Client Name', name: 'Client Name',
field: 'client_name', field: 'client_name',
placeholder: 'Awesome External App', placeholder: 'Awesome External App',
required: true, required: true,
type: 'text', type: 'text',
}, },
{ {
name: 'Redirect URI', name: 'Redirect URI',
field: 'redirect_uri', field: 'redirect_uri',
placeholder: 'https://awesome.app/oauth2/callback', placeholder: 'https://awesome.app/oauth2/callback',
required: true, required: true,
type: 'text', type: 'text',
}, },
{ {
name: 'Grant Types', name: 'Grant Types',
field: 'grant_types', field: 'grant_types',
type: 'select.multiple', type: 'select.multiple',
options: [ options: [
{ display: 'Refresh Token', value: 'refresh_token' }, {display: 'Refresh Token', value: 'refresh_token'},
{ display: 'Authorization Code', value: 'authorization_code' }, {display: 'Authorization Code', value: 'authorization_code'},
], ],
required: true, required: true,
}, },
{ {
name: 'Client ID', name: 'Client ID',
field: 'client_id', field: 'client_id',
type: 'text', type: 'text',
readonly: true, readonly: true,
hidden: ['insert'], hidden: ['insert'],
}, },
{ {
name: 'Client Secret', name: 'Client Secret',
field: 'client_secret', field: 'client_secret',
type: 'text', type: 'text',
readonly: true, readonly: true,
hidden: ['insert'], hidden: ['insert'],
}, },
], ],
}
} }
} }

View File

@ -0,0 +1,71 @@
import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js';
class ClientResource extends CRUDBase {
constructor() {
super()
this.endpoint = '/api/v1/radius/clients'
this.required_fields = ['name']
this.permission_base = 'v1:radius:clients'
this.item = 'RADIUS Client'
this.plural = 'RADIUS Clients'
this.listing_definition = {
display: ``,
columns: [
{
name: 'Client Name',
field: 'name',
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
this.form_definition = {
fields: [
{
name: 'Client Name',
field: 'name',
placeholder: 'Awesome External App',
required: true,
type: 'text',
},
{
name: 'Client Secret',
field: 'secret',
type: 'text',
readonly: true,
hidden: ['insert'],
},
],
}
}
}
const radius_client = new ClientResource()
export { radius_client }

View File

@ -1,12 +1,16 @@
import CRUDBase from '../CRUDBase.js' import CRUDBase from '../CRUDBase.js'
class ScopeResource extends CRUDBase { class ScopeResource extends CRUDBase {
endpoint = '/api/v1/reflect/scopes' constructor() {
required_fields = ['scope'] super()
permission_base = 'v1:reflect:scopes'
item = 'API Scope' this.endpoint = '/api/v1/reflect/scopes'
plural = 'API Scopes' this.required_fields = ['scope']
this.permission_base = 'v1:reflect:scopes'
this.item = 'API Scope'
this.plural = 'API Scopes'
}
} }
const reflect_scope = new ScopeResource() const reflect_scope = new ScopeResource()

View File

@ -1,87 +1,90 @@
import CRUDBase from '../CRUDBase.js' import CRUDBase from '../CRUDBase.js'
class TokenResource extends CRUDBase { class TokenResource extends CRUDBase {
endpoint = '/api/v1/reflect/tokens' constructor() {
required_fields = ['client_id'] super()
permission_base = 'v1:reflect:tokens' this.endpoint = '/api/v1/reflect/tokens'
this.required_fields = ['client_id']
this.permission_base = 'v1:reflect:tokens'
item = 'API Token' this.item = 'API Token'
plural = 'API Tokens' this.plural = 'API Tokens'
listing_definition = { this.listing_definition = {
display: ` display: `
This allows you to create bearer tokens manually to allow for easier testing of the API. Notably, this is meant as a measure for testing and development, not for long term use. This allows you to create bearer tokens manually to allow for easier testing of the API. Notably, this is meant as a measure for testing and development, not for long term use.
<br><br> <br><br>
If you have an application that needs to regularly interact with the API, set it up as an <a href="/dash/c/listing/oauth/Client">OAuth2 Client</a>. Manually-created tokens expire 7 days after their creation. If you have an application that needs to regularly interact with the API, set it up as an <a href="/dash/c/listing/oauth/Client">OAuth2 Client</a>. Manually-created tokens expire 7 days after their creation.
`, `,
columns: [ columns: [
{ {
name: 'Token', name: 'Token',
field: 'token', field: 'token',
},
{
name: 'Client',
field: 'client_display',
},
{
name: 'Expires',
field: 'expires',
},
],
actions: [
{
type: 'resource',
position: 'main',
action: 'insert',
text: 'Create New',
color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
form_definition = {
fields: [
{
name: 'Client',
field: 'client_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'oauth/Client',
display: 'name',
value: 'uuid',
}, },
}, {
{ name: 'Client',
name: 'Bearer Token', field: 'client_display',
field: 'token', },
type: 'text', {
readonly: true, name: 'Expires',
hidden: ['insert'], field: 'expires',
}, },
{ ],
name: 'Expires', actions: [
field: 'expires', {
type: 'text', type: 'resource',
readonly: true, position: 'main',
hidden: ['insert'], action: 'insert',
}, text: 'Create New',
], color: 'success',
},
{
type: 'resource',
position: 'row',
action: 'update',
icon: 'fa fa-edit',
color: 'primary',
},
{
type: 'resource',
position: 'row',
action: 'delete',
icon: 'fa fa-times',
color: 'danger',
confirm: true,
},
],
}
this.form_definition = {
fields: [
{
name: 'Client',
field: 'client_id',
required: true,
type: 'select.dynamic',
options: {
resource: 'oauth/Client',
display: 'name',
value: 'uuid',
},
},
{
name: 'Bearer Token',
field: 'token',
type: 'text',
readonly: true,
hidden: ['insert'],
},
{
name: 'Expires',
field: 'expires',
type: 'text',
readonly: true,
hidden: ['insert'],
},
],
}
} }
} }

View File

@ -2,92 +2,96 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js' import { session } from '../../service/Session.service.js'
class ProviderResource extends CRUDBase { class ProviderResource extends CRUDBase {
endpoint = '/api/v1/saml/providers' constructor() {
required_fields = ['name', 'acs_url', 'entity_id'] super()
permission_base = 'v1:saml:providers'
item = 'SAML Service Provider' this.endpoint = '/api/v1/saml/providers'
plural = 'SAML Service Providers' this.required_fields = ['name', 'acs_url', 'entity_id']
this.permission_base = 'v1:saml:providers'
listing_definition = { this.item = 'SAML Service Provider'
display: `SAML Service Providers are applications that support external authentication to a SAML Identity Provider. In this case, ${session.get('app.name')} is the identity provider, so these external applications can authenticate against it. this.plural = 'SAML Service Providers'
this.listing_definition = {
display: `SAML Service Providers are applications that support external authentication to a SAML Identity Provider. In this case, ${session.get('app.name')} is the identity provider, so these external applications can authenticate against it.
<br><br> <br><br>
To do this, you need to know the SAML service provider's entity ID, assertion consumer service URL, and single-logout URL (if supported).`, To do this, you need to know the SAML service provider's entity ID, assertion consumer service URL, and single-logout URL (if supported).`,
columns: [ columns: [
{ {
name: 'Provider Name', name: 'Provider Name',
field: 'name', field: 'name',
}, },
{ {
name: 'Entity ID', name: 'Entity ID',
field: 'entity_id', field: 'entity_id',
}, },
{ {
name: 'Has SLO?', name: 'Has SLO?',
field: 'slo_url', field: 'slo_url',
renderer: 'boolean', renderer: 'boolean',
}, },
{ {
name: 'ACS URL', name: 'ACS URL',
field: 'acs_url', field: 'acs_url',
}, },
], ],
actions: [ actions: [
{ {
type: 'resource', type: 'resource',
position: 'main', position: 'main',
action: 'insert', action: 'insert',
text: 'Create New', text: 'Create New',
color: 'success', color: 'success',
}, },
{ {
type: 'resource', type: 'resource',
position: 'row', position: 'row',
action: 'update', action: 'update',
icon: 'fa fa-edit', icon: 'fa fa-edit',
color: 'primary', color: 'primary',
}, },
{ {
type: 'resource', type: 'resource',
position: 'row', position: 'row',
action: 'delete', action: 'delete',
icon: 'fa fa-times', icon: 'fa fa-times',
color: 'danger', color: 'danger',
confirm: true, confirm: true,
}, },
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Provider Name', name: 'Provider Name',
field: 'name', field: 'name',
placeholder: 'Awesome External App', placeholder: 'Awesome External App',
required: true, required: true,
type: 'text', type: 'text',
}, },
{ {
name: 'Entity ID', name: 'Entity ID',
field: 'entity_id', field: 'entity_id',
placeholder: 'https://my.awesome.app/saml/metadata.xml', placeholder: 'https://my.awesome.app/saml/metadata.xml',
required: true, required: true,
type: 'text', type: 'text',
}, },
{ {
name: 'Assertion Consumer Service URL', name: 'Assertion Consumer Service URL',
field: 'acs_url', field: 'acs_url',
placeholder: 'https://my.awesome.app/saml/acs', placeholder: 'https://my.awesome.app/saml/acs',
required: true, required: true,
type: 'text', type: 'text',
}, },
{ {
name: 'Single-Logout URL', name: 'Single-Logout URL',
field: 'slo_url', field: 'slo_url',
placeholder: 'https://my.awesome.app/saml/logout', placeholder: 'https://my.awesome.app/saml/logout',
type: 'text', type: 'text',
}, },
], ],
}
} }
} }

View File

@ -1,95 +1,99 @@
import CRUDBase from '../CRUDBase.js' import CRUDBase from '../CRUDBase.js'
class AnnouncementResource extends CRUDBase { class AnnouncementResource extends CRUDBase {
endpoint = '/api/v1/system/announcements' constructor() {
required_fields = ['user_ids', 'group_ids', 'title', 'message', 'type'] super()
permission_base = 'v1:system:announcements'
item = 'System Announcement' this.endpoint = '/api/v1/system/announcements'
plural = 'System Announcements' this.required_fields = ['user_ids', 'group_ids', 'title', 'message', 'type']
this.permission_base = 'v1:system:announcements'
listing_definition = { this.item = 'System Announcement'
display: ` this.plural = 'System Announcements'
this.listing_definition = {
display: `
System announcements are administrative messages that you want all or some of your users to see. These messages can be delivered via e-mail, as a message after login, or as a system banner announcement. System announcements are administrative messages that you want all or some of your users to see. These messages can be delivered via e-mail, as a message after login, or as a system banner announcement.
`, `,
columns: [ columns: [
{ {
name: 'Title', name: 'Title',
field: 'title', field: 'title',
}, },
{ {
name: 'Message', name: 'Message',
field: 'message', field: 'message',
renderer: (message) => String(message).slice(0, 150), renderer: (message) => String(message).slice(0, 150),
}, },
], ],
actions: [ actions: [
{ {
type: 'resource', type: 'resource',
position: 'main', position: 'main',
action: 'insert', action: 'insert',
text: 'Create New', text: 'Create New',
color: 'success', color: 'success',
}, },
{ {
type: 'resource', type: 'resource',
position: 'row', position: 'row',
action: 'delete', action: 'delete',
icon: 'fa fa-times', icon: 'fa fa-times',
color: 'danger', color: 'danger',
confirm: true, confirm: true,
}, },
], ],
} }
form_definition = { this.form_definition = {
fields: [ fields: [
{ {
name: 'Title', name: 'Title',
field: 'title', field: 'title',
type: 'text', type: 'text',
},
{
name: 'Message',
field: 'message',
type: 'textarea',
},
{
name: 'Users',
field: 'user_ids',
type: 'select.dynamic.multiple',
options: {
resource: 'auth/User',
display: (user) => `${user.last_name}, ${user.first_name} (${user.uid})`,
value: 'id',
}, },
}, {
{ name: 'Message',
name: 'Groups', field: 'message',
field: 'group_ids', type: 'textarea',
type: 'select.dynamic.multiple',
options: {
resource: 'auth/Group',
display: (group) => `${group.name}`,
value: 'id',
}, },
}, {
{ name: 'Users',
name: 'Type', field: 'user_ids',
field: 'type', type: 'select.dynamic.multiple',
type: 'select', options: {
options: [ resource: 'auth/User',
{ display: 'Login Intercept', value: 'login' }, display: (user) => `${user.last_name}, ${user.first_name} (${user.uid})`,
{ display: 'E-Mail', value: 'email' }, value: 'id',
{ display: 'System Banner', value: 'banner' }, },
], },
}, {
], name: 'Groups',
handlers: { field: 'group_ids',
insert: { type: 'select.dynamic.multiple',
action: 'redirect', options: {
next: '/dash/c/listing/system/Announcement', resource: 'auth/Group',
}, display: (group) => `${group.name}`,
value: 'id',
},
},
{
name: 'Type',
field: 'type',
type: 'select',
options: [
{display: 'Login Intercept', value: 'login'},
{display: 'E-Mail', value: 'email'},
{display: 'System Banner', value: 'banner'},
],
},
],
handlers: {
insert: {
action: 'redirect',
next: '/dash/c/listing/system/Announcement',
},
}
} }
} }
} }

View File

@ -1,5 +1,11 @@
import { location_service } from './Location.service.js' import { location_service } from './Location.service.js'
import { resource_service } from './Resource.service.js' import { resource_service } from './Resource.service.js'
import { event_bus } from './EventBus.service.js'
const pageMap = {
'dash.profile': '/dash/profile',
'app.setup': '/dash/app/setup',
}
class ActionService { class ActionService {
async perform({ text = '', action, ...args }) { async perform({ text = '', action, ...args }) {
@ -7,21 +13,44 @@ class ActionService {
if ( args.next ) { if ( args.next ) {
return location_service.redirect(args.next, args.delay || 0) return location_service.redirect(args.next, args.delay || 0)
} }
} else if ( action === 'navigate' ) {
if ( args.page && pageMap[args.page] ) {
window.history.pushState('pageNavigate', `Open ${args.page}`, pageMap[args.page])
return event_bus.event('root.navigate').fire(args)
}
} else if ( action === 'back' ) { } else if ( action === 'back' ) {
return location_service.back() return location_service.back()
} else if ( args.type === 'resource' ) { } else if ( args.type === 'resource' ) {
const { resource } = args const { resource } = args
if ( action === 'insert' ) { if ( action === 'insert' ) {
return location_service.redirect(`/dash/c/form/${resource}`, 0) window.history.pushState('cobaltForm', `Insert ${resource}`, `/dash/c/form/${resource}`)
return event_bus.event('root.navigate').fire({
page: 'cobalt.form',
resource,
mode: 'insert',
})
} else if ( action === 'update' ) { } else if ( action === 'update' ) {
const { id } = args const { id } = args
return location_service.redirect(`/dash/c/form/${resource}?id=${id}`, 0) window.history.pushState('cobaltForm', `Edit ${resource}`, `/dash/c/form/${resource}?id=${id}`)
return event_bus.event('root.navigate').fire({
page: 'cobalt.form',
resource,
mode: 'update',
form_id: id,
})
} else if ( action === 'delete' ) { } else if ( action === 'delete' ) {
const { id } = args const { id } = args
const rsc = await resource_service.get(resource) const rsc = await resource_service.get(resource)
await rsc.delete(id) await rsc.delete(id)
} else if ( action === 'list' ) { } else if ( action === 'list' ) {
return location_service.redirect(`/dash/c/listing/${resource}`, 0) window.history.pushState('cobaltListing', `View ${resource}`, `/dash/c/listing/${resource}`)
return event_bus.event('root.navigate').fire({
page: 'cobalt.listing',
resource,
})
} }
} else if ( action === 'post' ) { } else if ( action === 'post' ) {
const inputs = [] const inputs = []

View File

@ -1,9 +1,9 @@
class Event { class Event {
firings = []
subscriptions = []
constructor(name) { constructor(name) {
this.name = name this.name = name
this.firings = []
this.subscriptions = []
} }
subscribe(handler) { subscribe(handler) {
@ -22,7 +22,9 @@ class Event {
} }
class EventBusService { class EventBusService {
_events = {} constructor() {
this._events = {}
}
event(name) { event(name) {
if ( !this._events[name] ) { if ( !this._events[name] ) {

View File

@ -2,7 +2,9 @@ import { event_bus } from './EventBus.service.js'
import { auth_api } from './AuthApi.service.js' import { auth_api } from './AuthApi.service.js'
class MessageService { class MessageService {
listener_interval = 25000 constructor() {
this.listener_interval = 25000
}
alert({type, message, timeout = 0, on_dismiss = () => {} }) { alert({type, message, timeout = 0, on_dismiss = () => {} }) {
event_bus.event('message.alert').fire({ type, message, timeout, on_dismiss }) event_bus.event('message.alert').fire({ type, message, timeout, on_dismiss })

View File

@ -1,3 +1,5 @@
import {message_service} from './Message.service.js'
class ProfileService { class ProfileService {
async get_profile(user_id = 'me') { async get_profile(user_id = 'me') {
@ -10,8 +12,11 @@ class ProfileService {
if ( results && results.data && results.data.data ) return results.data.data if ( results && results.data && results.data.data ) return results.data.data
} }
async update_profile({ user_id, first_name, last_name, email, tagline = undefined }) { async update_profile({ user_id, first_name, last_name, email, login_shell = undefined, tagline = undefined }) {
await axios.patch(`/api/v1/profile/${user_id}`, { first_name, last_name, email, tagline }) const results = await axios.patch(`/api/v1/profile/${user_id}`, { first_name, last_name, email, tagline, login_shell })
if ( results && results.data && results.data.data && results.data.data.force_message_refresh ) {
await message_service._listener_tick()
}
} }
async update_notify({ user_id = 'me', app_key, gateway_url }) { async update_notify({ user_id = 'me', app_key, gateway_url }) {

View File

@ -1,5 +1,7 @@
class Session { class Session {
data = {} constructor() {
this.data = {}
}
init(data) { init(data) {
this.data = data this.data = data

View File

@ -1,5 +1,7 @@
class TranslateService { class TranslateService {
_cache = {} constructor() {
this._cache = {}
}
check_cache(...keys) { check_cache(...keys) {
const obj = {} const obj = {}

View File

@ -1,5 +1,7 @@
class UtilityService { class UtilityService {
_debounce_timeouts = {} constructor() {
this._debounce_timeouts = {}
}
uuid() { uuid() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>

35
app/assets/error-log.js Normal file
View File

@ -0,0 +1,35 @@
window.COREID_ERROR_LOG_URL = window.COREID_ERROR_LOG_URL || '/api/v1/log-error'
async function logError(error) {
try {
await fetch(window.COREID_ERROR_LOG_URL, {
method: 'POST',
cache: 'no-cache',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
full_url: window.location.href,
trace: [
error.name + ': ' + error.message,
error.stack,
].join('\n')
}),
})
} catch (e) {}
}
;(function() {
var old_onerror = window.onerror
window.onerror = function(msg, src, line, col, error) {
logError(error).then(function() {
if ( typeof old_onerror === 'function' ) {
try {
old_onerror(msg, src, line, col, error)
} catch(e) {}
}
})
}
})()

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="info-circle" class="svg-inline--fa fa-info-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"></path></svg>

After

Width:  |  Height:  |  Size: 641 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -18,6 +18,11 @@ class CoreIDAdapter {
expiresAt = new Date(Date.now() + (expiresIn * 1000)) expiresAt = new Date(Date.now() + (expiresIn * 1000))
} }
if ( payload.uid ) {
payload.originalUid = payload.uid
payload.uid = payload.uid.toLowerCase()
}
await this.coll().updateOne( await this.coll().updateOne(
{ _id }, { _id },
{ $set: { payload, ...(expiresAt ? { expiresAt } : undefined) } }, { $set: { payload, ...(expiresAt ? { expiresAt } : undefined) } },
@ -34,6 +39,11 @@ class CoreIDAdapter {
).limit(1).next() ).limit(1).next()
if (!result) return undefined if (!result) return undefined
if ( result?.payload?.originalUid ) {
result.payload.uid = result.payload.originalUid
}
return result.payload return result.payload
} }
@ -49,11 +59,16 @@ class CoreIDAdapter {
async findByUid(uid) { async findByUid(uid) {
const result = await this.coll().find( const result = await this.coll().find(
{ 'payload.uid': uid }, { 'payload.uid': uid.toLowerCase() },
{ payload: 1 }, { payload: 1 },
).limit(1).next() ).limit(1).next()
if (!result) return undefined if (!result) return undefined
if ( result?.payload?.originalUid ) {
result.payload.uid = result.payload.originalUid
}
return result.payload return result.payload
} }

View File

@ -0,0 +1,55 @@
const User = require('../../models/auth/User.model')
const Client = require('../../models/radius/Client.model')
const Application = require('../../models/Application.model')
const Policy = require('../../models/iam/Policy.model')
/**
* @implements IAuthentication from radius-server
*/
class CoreIDAuthentication {
async authenticate(username, password, packet) {
// We only allow client-specific secrets to authenticate
if ( !packet || !packet.secret ) {
return false;
}
// Try to look up the client
const client = await Client.findOne({
active: true,
secret: packet.secret,
})
if ( !client ) {
return false;
}
// Try to look up the associated application
const application = await Application.findOne({
radius_client_ids: client.id,
})
if ( !application ) {
return false;
}
// Try to look up the user
/** @var {User} */
const user = await User.findByLogin(username)
if ( !user ) {
return false;
}
// Validate the incoming credential
if ( !(await user.check_credential_string(password)) ) {
return false;
}
// Don't allow login if the user has a trap set
if ( user.trap ) {
return false;
}
// Check the IAM policy engine to make sure the user can access this resource
return Policy.check_user_access(user, application.id)
}
}
module.exports = exports = CoreIDAuthentication

View File

@ -0,0 +1,28 @@
import radius from 'radius'
import { RadiusServer } from '@coreid/radius-server'
import RadiusClient from '../../models/radius/Client.model.js'
import CoreIDUserPasswordPacketHandler from './CoreIDUserPasswordPacketHandler.mjs'
export default class CoreIDRadiusServer extends RadiusServer {
// constructor(options) {
// super(options)
// this.packetHandler.packetHandlers.pop()
// this.packetHandler.packetHandlers.push(new CoreIDUserPasswordPacketHandler(options.authentication, this.logger))
// console.log(this.packetHandler.packetHandlers)
// }
async decodeMessage(msg) {
const clients = await RadiusClient.find({ active: true })
for ( const client of clients ) {
try {
const packet = radius.decode({ packet: msg, secret: client.secret })
packet.secret = client.secret
return packet
} catch (e) {
console.error(e)
}
}
throw new Error('Unable to determine client to decode RADIUS packet: is the client active?')
}
}

View File

@ -0,0 +1,40 @@
import { UserPasswordPacketHandler } from '@coreid/radius-server/dist/radius/handler/UserPasswordPacketHandler.js'
export default class CoreIDUserPasswordPacketHandler extends UserPasswordPacketHandler {
async handlePacket(packet) {
console.log('coreid user password packet handler handlePacket', packet)
const username = packet.attributes['User-Name'];
let password = packet.attributes['User-Password'];
if (Buffer.isBuffer(password) && password.indexOf(0x00) > 0) {
// check if there is a 0x00 in it, and trim it from there
password = password.slice(0, password.indexOf(0x00));
}
if (!username || !password) {
// params missing, this handler cannot continue...
return {};
}
this.logger.debug('username', username, username.toString());
this.logger.debug('token', password, password.toString());
console.log('client', packet.__coreid_client)
const authenticated = await this.authentication.authenticate(
username.toString(),
password.toString()
);
if (authenticated) {
// success
return {
code: 'Access-Accept',
attributes: [['User-Name', username]],
};
}
// Failed
return {
code: 'Access-Reject',
};
}
}

View File

@ -43,7 +43,7 @@ class FlitterProfileMapper {
getClaims() { getClaims() {
const claims = {} const claims = {}
claims[this.map.nameIdentifier] = this.user.uid claims[this.map.nameIdentifier] = this.user.uid.toLowerCase()
claims[this.map.email] = this.user.email claims[this.map.email] = this.user.email
claims[this.map.name] = `${this.user.first_name} ${this.user.last_name}` claims[this.map.name] = `${this.user.first_name} ${this.user.last_name}`
claims[this.map.givenname] = this.user.first_name claims[this.map.givenname] = this.user.first_name
@ -54,7 +54,7 @@ class FlitterProfileMapper {
} }
getNameIdentifier() { getNameIdentifier() {
return { nameIdentifier: this.user.uid } return { nameIdentifier: this.user.uid.toLowerCase() }
} }
} }

View File

@ -29,6 +29,12 @@ class Home extends Controller {
async tmpl(req, res) { async tmpl(req, res) {
return res.page('tmpl', {...this.Vue.data(), ...this.Vue.session(req)}) return res.page('tmpl', {...this.Vue.data(), ...this.Vue.session(req)})
} }
async log_front_end_error(req, res, next) {
const FrontEndError = this.models.get('FrontEndError')
await FrontEndError.log(req)
return res.api()
}
} }
module.exports = Home module.exports = Home

View File

@ -105,7 +105,7 @@ class OpenIDController extends Controller {
const Client = this.models.get('openid:Client') const Client = this.models.get('openid:Client')
const client = await Client.findById(req.params.id) const client = await Client.findById(req.params.id)
if ( !client || !client.active ) if ( !client )
return res.status(404) return res.status(404)
.message(req.T('api.client_not_found')) .message(req.T('api.client_not_found'))
.api() .api()
@ -119,14 +119,12 @@ class OpenIDController extends Controller {
uid, prompt, params, session, uid, prompt, params, session,
} = await this.openid_connect.provider.interactionDetails(req, res) } = await this.openid_connect.provider.interactionDetails(req, res)
console.log({uid, prompt, params, session})
const name = prompt.name const name = prompt.name
if ( typeof this[name] !== 'function' ) { if ( typeof this[name] !== 'function' ) {
return this.fail(res, 'Sorry, something has gone wrong.') return this.fail(res, 'Sorry, something has gone wrong.')
} }
return this[name](req, res, { uid, prompt, params, session }) return this[name](req, res, { uid: uid.toLowerCase(), prompt, params, session })
} }
async consent(req, res, { uid, prompt, params, session }) { async consent(req, res, { uid, prompt, params, session }) {
@ -142,19 +140,25 @@ class OpenIDController extends Controller {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
const application = await Application.findOne({ openid_client_ids: params.client_id }) const application = await Application.findOne({ openid_client_ids: params.client_id })
if ( !application ) { if ( !application ) {
this.output.warning('IAM Denial!') this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, { return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', 'this application'), message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
next_destination: '/dash', next_destination: '/dash',
}) })
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) { } else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
this.output.warning('IAM Denial!') this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, { return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name), message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash', next_destination: '/dash',
}) })
} }
// If the user has already authorized this app, just redirect
if ( req.user.has_authorized({ id: application.id }) ) {
return res.redirect(`/openid/interaction/${uid.toLowerCase()}/grant`)
}
// Otherwise, prompt them for authorization
return res.page('public:message', { return res.page('public:message', {
...this.Vue.data({ ...this.Vue.data({
message: `<h3 class="font-weight-light">Authorize ${application.name}?</h3> message: `<h3 class="font-weight-light">Authorize ${application.name}?</h3>
@ -172,15 +176,33 @@ class OpenIDController extends Controller {
{ {
text: req.T('common.grant'), text: req.T('common.grant'),
action: 'redirect', action: 'redirect',
next: `/openid/interaction/${uid}/grant`, next: `/openid/grant-and-save/${application.id}/${uid.toLowerCase()}`,
},
{
text: req.T('common.grant_once'),
action: 'redirect',
next: `/openid/interaction/${uid.toLowerCase()}/grant`,
}, },
], ],
}) })
}) })
} }
async grant_and_save(req, res, next) {
if ( !req.user.has_authorized({ id: req.params.app_id }) ) {
req.user.authorize({
id: req.params.app_id,
api_scopes: ['openid-connect'],
})
await req.user.save()
}
return res.redirect(`/openid/interaction/${req.params.uid.toLowerCase()}/grant`)
}
async login(req, res, { uid, prompt, params, session }) { async login(req, res, { uid, prompt, params, session }) {
return res.redirect(`/openid/interaction/${uid}/start-session`) return res.redirect(`/openid/interaction/${uid.toLowerCase()}/start-session`)
} }
/** /**
@ -202,13 +224,13 @@ class OpenIDController extends Controller {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
const application = await Application.findOne({ openid_client_ids: params.client_id }) const application = await Application.findOne({ openid_client_ids: params.client_id })
if ( !application ) { if ( !application ) {
this.output.warning('IAM Denial!') this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, { return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', 'this application'), message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
next_destination: '/dash', next_destination: '/dash',
}) })
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) { } else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
this.output.warning('IAM Denial!') this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, { return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name), message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash', next_destination: '/dash',
@ -238,13 +260,13 @@ class OpenIDController extends Controller {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
const application = await Application.findOne({ openid_client_ids: params.client_id }) const application = await Application.findOne({ openid_client_ids: params.client_id })
if ( !application ) { if ( !application ) {
this.output.warning('IAM Denial!') this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, { return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', 'this application'), message: req.T('saml.no_access').replace('APP_NAME', 'this application'),
next_destination: '/dash', next_destination: '/dash',
}) })
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) { } else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
this.output.warning('IAM Denial!') this.output.warn('IAM Denial!')
return this.Vue.auth_message(res, { return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name), message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash', next_destination: '/dash',

View File

@ -115,6 +115,28 @@ class AppController extends Controller {
application.oauth_client_ids = oauth_client_ids application.oauth_client_ids = oauth_client_ids
} }
// Verify RADIUS client IDs
const RadiusClient = this.models.get('radius:Client')
if ( req.body.radius_client_ids ) {
const parsed = typeof req.body.radius_client_ids === 'string' ? this.utility.infer(req.body.radius_client_ids) : req.body.radius_client_ids
const radius_client_ids = Array.isArray(parsed) ? parsed : [parsed]
for ( const id of radius_client_ids ) {
const client = await RadiusClient.findById(id)
if ( !client || !client.active || !req.user.can(`radius:client:${client.id}:view`) )
return res.status(400)
.message(`${req.T('api.invalid_radius_client_id')} ${id}`)
.api()
const other_assoc_app = await Application.findOne({ radius_client_ids: client.id })
if ( other_assoc_app )
return res.status(400) // TODO translate this
.message(`The RADIUS client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
.api()
}
application.radius_client_ids = radius_client_ids
}
// Verify OpenID client IDs // Verify OpenID client IDs
const OpenIDClient = this.models.get('openid:Client') const OpenIDClient = this.models.get('openid:Client')
if ( req.body.openid_client_ids ) { if ( req.body.openid_client_ids ) {
@ -242,6 +264,28 @@ class AppController extends Controller {
application.oauth_client_ids = oauth_client_ids application.oauth_client_ids = oauth_client_ids
} else application.oauth_client_ids = [] } else application.oauth_client_ids = []
// Verify OAuth client IDs
const RadiusClient = this.models.get('radius:Client')
if ( req.body.radius_client_ids ) {
const parsed = typeof req.body.radius_client_ids === 'string' ? this.utility.infer(req.body.radius_client_ids) : req.body.radius_client_ids
const radius_client_ids = Array.isArray(parsed) ? parsed : [parsed]
for ( const id of radius_client_ids ) {
const client = await RadiusClient.findById(id)
if ( !client || !client.active || !req.user.can(`radius:client:${client.id}:view`) )
return res.status(400)
.message(`${req.T('api.invalid_radius_client_id')} ${id}`)
.api()
const other_assoc_app = await Application.findOne({ radius_client_ids: client.id })
if ( other_assoc_app && other_assoc_app.id !== application.id )
return res.status(400) // TODO translate this
.message(`The RADIUS client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
.api()
}
application.radius_client_ids = radius_client_ids
} else application.radius_client_ids = []
// Verify OpenID client IDs // Verify OpenID client IDs
const OpenIDClient = this.models.get('openid:Client') const OpenIDClient = this.models.get('openid:Client')
if ( req.body.openid_client_ids ) { if ( req.body.openid_client_ids ) {

View File

@ -71,7 +71,7 @@ class AuthController extends Controller {
const user = new User({ const user = new User({
first_name: req.body.first_name, first_name: req.body.first_name,
last_name: req.body.last_name, last_name: req.body.last_name,
uid: req.body.uid, uid: req.body.uid.toLowerCase(),
email: req.body.email, email: req.body.email,
trap: 'password_reset', // Force user to reset password trap: 'password_reset', // Force user to reset password
}) })
@ -91,6 +91,7 @@ class AuthController extends Controller {
if ( !(await User.findOne()) ) user.promote('root') if ( !(await User.findOne()) ) user.promote('root')
await user.save() await user.save()
await user.grant_defaults()
// Log in the user automatically // Log in the user automatically
await this.auth.get_provider().session(req, user) await this.auth.get_provider().session(req, user)
@ -219,6 +220,48 @@ class AuthController extends Controller {
return res.api(await user.to_api()) return res.api(await user.to_api())
} }
async get_user_flat(req, res, next) {
if ( req.params.id === 'me' )
return res.json(await req.user.to_api())
const User = this.models.get('auth:User')
const user = await User.findById(req.params.id)
if ( !user )
return res.status(404)
.message(req.T('api.user_not_found'))
.api()
if ( !req.user.can(`auth:user:${user.id}:view`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
return res.json(await user.to_api())
}
async get_user_photo(req, res, next) {
let user
if ( req.params.id === 'me' ) {
user = req.user
} else {
const User = this.models.get('auth:User')
user = await User.findOne({ uid: req.params.id })
}
if ( !user )
return res.status(404)
.message(req.T('api.user_not_found'))
.api()
const file = await user.photo()
if ( !file )
// The user does not have a profile. Send the default.
return res.sendFile(this.utility.path('app/assets/people.png'))
await file.send(res)
}
async create_group(req, res, next) { async create_group(req, res, next) {
if ( !req.user.can(`auth:group:create`) ) if ( !req.user.can(`auth:group:create`) )
return res.status(401) return res.status(401)
@ -239,7 +282,10 @@ class AuthController extends Controller {
.message(req.T('api.group_already_exists')) .message(req.T('api.group_already_exists'))
.api() .api()
const group = new Group({ name: req.body.name }) const group = new Group({
name: req.body.name,
grants_sudo: !!req.body.grants_sudo,
})
// Validate user ids // Validate user ids
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
@ -258,6 +304,7 @@ class AuthController extends Controller {
} }
await group.save() await group.save()
await group.get_gid_number()
return res.api(await group.to_api()) return res.api(await group.to_api())
} }
@ -297,7 +344,7 @@ class AuthController extends Controller {
.api() .api()
const user = new User({ const user = new User({
uid: req.body.uid, uid: req.body.uid.toLowerCase(),
email: req.body.email, email: req.body.email,
first_name: req.body.first_name, first_name: req.body.first_name,
last_name: req.body.last_name, last_name: req.body.last_name,
@ -317,6 +364,7 @@ class AuthController extends Controller {
await user.reset_password(req.body.password, 'create') await user.reset_password(req.body.password, 'create')
await user.save() await user.save()
await user.grant_defaults()
return res.api(await user.to_api()) return res.api(await user.to_api())
} }
@ -365,7 +413,10 @@ class AuthController extends Controller {
} }
group.name = req.body.name group.name = req.body.name
group.grants_sudo = !!req.body.grants_sudo
await group.save() await group.save()
await group.get_gid_number()
return res.api() return res.api()
} }
@ -417,7 +468,7 @@ class AuthController extends Controller {
user.first_name = req.body.first_name user.first_name = req.body.first_name
user.last_name = req.body.last_name user.last_name = req.body.last_name
user.uid = req.body.uid user.uid = req.body.uid.toLowerCase()
user.email = req.body.email user.email = req.body.email
if ( req.body.tagline ) if ( req.body.tagline )
@ -493,7 +544,7 @@ class AuthController extends Controller {
if ( is_valid ) { if ( is_valid ) {
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
const user = await User.findOne({uid: req.body.username}) const user = await User.findOne({uid: req.body.username.toLowerCase()})
if ( !user || !user.can_login ) is_valid = false if ( !user || !user.can_login ) is_valid = false
} }
@ -511,7 +562,7 @@ class AuthController extends Controller {
const data = {} const data = {}
if ( req.body.username ) { if ( req.body.username ) {
const existing_user = await User.findOne({ const existing_user = await User.findOne({
uid: req.body.username, uid: req.body.username.toLowerCase(),
}) })
data.username_taken = !!existing_user data.username_taken = !!existing_user
@ -544,7 +595,8 @@ class AuthController extends Controller {
.message(req.T('auth.unable_to_complete')) .message(req.T('auth.unable_to_complete'))
.api({ errors }) .api({ errors })
const login_args = await flitter.get_login_args(req.body) const [username, ...other_args] = await flitter.get_login_args(req.body)
const login_args = [username.toLowerCase(), ...other_args]
const user = await flitter.login.apply(flitter, login_args) const user = await flitter.login.apply(flitter, login_args)
if ( !user ) if ( !user )

View File

@ -13,7 +13,7 @@ class IAMController extends Controller {
.message(`${req.T('api.missing_field', true)} entity_id, target_id`) .message(`${req.T('api.missing_field', true)} entity_id, target_id`)
.api() .api()
return res.api(await Policy.check_entity_access(req.body.entity_id, req.body.target_id)) return res.api(await Policy.check_entity_access(req.body.entity_id, req.body.target_id, req.body.permission || undefined))
} }
async check_user_access(req, res, next) { async check_user_access(req, res, next) {
@ -39,7 +39,7 @@ class IAMController extends Controller {
.message(req.T('api.insufficient_permissions')) .message(req.T('api.insufficient_permissions'))
.api() .api()
return res.api(await Policy.check_user_access(user, req.body.target_id)) return res.api(await Policy.check_user_access(user, req.body.target_id, req.body.permission || undefined))
} }
async get_policies(req, res, next) { async get_policies(req, res, next) {
@ -56,6 +56,33 @@ class IAMController extends Controller {
return res.api(data) return res.api(data)
} }
async get_permissions(req, res, next) {
const Permission = this.models.get('iam:Permission')
const permissions = await Permission.find({
active: true,
...(req.query.target_type ? {
target_type: req.query.target_type,
} : {})
})
const data = []
for ( const perm of permissions ) {
if ( req.user.can(`iam:permission:${perm.target_type}:view`) ) {
data.push(await perm.to_api())
}
}
if ( req.query.include_unset ) {
data.reverse().push({
permission: '',
})
data.reverse()
}
return res.api(data)
}
async get_policy(req, res, next) { async get_policy(req, res, next) {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
const policy = await Policy.findById(req.params.id) const policy = await Policy.findById(req.params.id)
@ -73,6 +100,23 @@ class IAMController extends Controller {
return res.api(await policy.to_api()) return res.api(await policy.to_api())
} }
async get_permission(req, res, next) {
const Permission = this.models.get('iam:Permission')
const permission = await Permission.findById(req.params.id)
if ( !permission )
return res.status(404)
.message(req.T('iam.permission_not_found'))
.api()
if ( !req.user.can(`iam:permission:${permission.target_type}:view`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
return res.api(await permission.to_api())
}
async create_policy(req, res, next) { async create_policy(req, res, next) {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
@ -108,12 +152,12 @@ class IAMController extends Controller {
if ( !['allow', 'deny'].includes(req.body.access_type) ) if ( !['allow', 'deny'].includes(req.body.access_type) )
return res.status(400) return res.status(400)
.message(`${req.T('common.invalid')} access_type. ${req.T('api:must_one')} allow, deny.`) .message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`)
.api() .api()
if ( !['application', 'api_scope'].includes(req.body.target_type) ) if ( !['application', 'api_scope', 'machine', 'machine_group'].includes(req.body.target_type) )
return res.status(400) return res.status(400)
.message(`${req.T('common.invalid')} target_type. ${req.T('api:must_one')} application, api_scope.`) .message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope, machine, machine_group.`)
.api() .api()
// Make sure the target_id is valid // Make sure the target_id is valid
@ -130,6 +174,20 @@ class IAMController extends Controller {
return res.status(400) return res.status(400)
.message(`${req.T('common.invalid')} target_id.`) .message(`${req.T('common.invalid')} target_id.`)
.api() .api()
} else if ( req.body.target_type === 'machine' ) {
const Machine = this.models.get('ldap:Machine')
const machine = await Machine.findById(req.body.target_id)
if ( !machine || !machine.active || !req.user.can(`ldap:machine:${machine.id}:view`) )
return res.status(400)
.message(`${req.T('common.invalid')} target_id.`)
.api()
} else if ( req.body.target_type === 'machine_group' ) {
const MachineGroup = this.models.get('ldap:MachineGroup')
const group = await MachineGroup.findById(req.body.target_id)
if ( !group || !group.active || !req.user.can(`ldap:machine_group:${group.id}:view`) )
return res.status(400)
.message(`${req.T('common.invalid')} target_id.`)
.api()
} }
const policy = new Policy({ const policy = new Policy({
@ -140,12 +198,71 @@ class IAMController extends Controller {
target_id: req.body.target_id, target_id: req.body.target_id,
}) })
if ( req.body.permission ) {
// Validate the permission and set it, if it is valid
const Permission = this.models.get('iam:Permission')
const permission = await Permission.findOne({
active: true,
target_type: req.body.target_type,
permission: req.body.permission,
})
if ( permission ) {
policy.for_permission = true
policy.permission = req.body.permission
}
}
await policy.save() await policy.save()
req.user.allow(`iam:policy:${policy.id}`) req.user.allow(`iam:policy:${policy.id}`)
await req.user.save() await req.user.save()
return res.api(await policy.to_api()) return res.api(await policy.to_api())
} }
async create_permission(req, res, next) {
const Permission = this.models.get('iam:Permission')
const required_fields = ['target_type', 'permission']
for ( const field of required_fields ) {
if ( !req.body[field] )
return res.status(400)
.message(`${req.T('api.missing_field')} ${field}`)
.api()
}
const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group']
if ( !valid_target_types.includes(req.body.target_type) ) {
return res.status(400)
.message(`${req.T('api.invalid_target_type')}`)
.api()
}
if ( !req.user.can(`iam:permission${req.body.target_type}:create`) ) {
return res.status(401).api()
}
// Make sure one doesn't already exist
const existing = await Permission.findOne({
active: true,
target_type: req.body.target_type,
permission: req.body.permission,
})
if ( existing ) {
return res.status(400)
.message(req.T('api.permission_already_exists'))
.api()
}
const perm = new Permission({
target_type: req.body.target_type,
permission: req.body.permission,
})
await perm.save()
return res.api(await perm.to_api())
}
async update_policy(req, res, next) { async update_policy(req, res, next) {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
const policy = await Policy.findById(req.params.id) const policy = await Policy.findById(req.params.id)
@ -195,9 +312,9 @@ class IAMController extends Controller {
.message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`) .message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`)
.api() .api()
if ( !['application', 'api_scope'].includes(req.body.target_type) ) if ( !['application', 'api_scope', 'machine', 'machine_group'].includes(req.body.target_type) )
return res.status(400) return res.status(400)
.message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope.`) .message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope, machine, machine_group.`)
.api() .api()
// Make sure the target_id is valid // Make sure the target_id is valid
@ -214,6 +331,20 @@ class IAMController extends Controller {
return res.status(400) return res.status(400)
.message(`${req.T('common.invalid')} target_id.`) .message(`${req.T('common.invalid')} target_id.`)
.api() .api()
} else if ( req.body.target_type === 'machine' ) {
const Machine = this.models.get('ldap:Machine')
const machine = await Machine.findById(req.body.target_id)
if ( !machine || !machine.active || !req.user.can(`ldap:machine:${machine.id}:view`) )
return res.status(400)
.message(`${req.T('common.invalid')} target_id.`)
.api()
} else if ( req.body.target_type === 'machine_group' ) {
const MachineGroup = this.models.get('ldap:MachineGroup')
const group = await MachineGroup.findById(req.body.target_id)
if ( !group || !group.active || !req.user.can(`ldap:machine_group:${group.id}:view`) )
return res.status(400)
.message(`${req.T('common.invalid')} target_id.`)
.api()
} }
policy.entity_type = req.body.entity_type policy.entity_type = req.body.entity_type
@ -221,10 +352,69 @@ class IAMController extends Controller {
policy.access_type = req.body.access_type policy.access_type = req.body.access_type
policy.target_type = req.body.target_type policy.target_type = req.body.target_type
policy.target_id = req.body.target_id policy.target_id = req.body.target_id
if ( req.body.permission ) {
// Validate the permission and set it, if it is valid
const Permission = this.models.get('iam:Permission')
const permission = await Permission.findOne({
active: true,
target_type: req.body.target_type,
permission: req.body.permission,
})
if ( permission ) {
policy.for_permission = true
policy.permission = req.body.permission
} else {
policy.for_permission = false
policy.permission = undefined
}
} else {
policy.for_permission = false
policy.permission = undefined
}
await policy.save() await policy.save()
return res.api() return res.api()
} }
async update_permission(req, res, next) {
const Permission = this.models.get('iam:Permission')
const required_fields = ['target_type', 'permission']
for ( const field of required_fields ) {
if ( !req.body[field] )
return res.status(400)
.message(`${req.T('api.missing_field')} ${field}`)
.api()
}
const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group']
if ( !valid_target_types.includes(req.body.target_type) ) {
return res.status(400)
.message(`${req.T('api.invalid_target_type')}`)
.api()
}
if ( !req.user.can(`iam:permission${req.body.target_type}:update`) ) {
return res.status(401).api()
}
// Make sure one doesn't already exist
const existing = await Permission.findById(req.params.id)
if ( !existing?.active ) {
return res.status(404)
.message(req.T('api.permission_not_found'))
.api()
}
existing.target_type = req.body.target_type
existing.permission = req.body.permission
await existing.save()
return res.api(await existing.to_api())
}
async delete_policy(req, res, next) { async delete_policy(req, res, next) {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
const policy = await Policy.findById(req.params.id) const policy = await Policy.findById(req.params.id)
@ -243,6 +433,27 @@ class IAMController extends Controller {
await policy.save() await policy.save()
return res.api() return res.api()
} }
async delete_permission(req, res, next) {
const Permission = this.models.get('iam:Permission')
const permission = await Permission.findById(req.params.id)
if ( !permission?.active ) {
return res.status(404)
.message(req.T('api.permission_not_found'))
.api()
}
if ( !req.user.can(`iam:permission:${permission.target_type}:delete`) ) {
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
}
permission.active = false
await permission.save()
return res.api()
}
} }
module.exports = exports = IAMController module.exports = exports = IAMController

View File

@ -46,6 +46,32 @@ class LDAPController extends Controller {
return res.api(data) return res.api(data)
} }
async get_machines(req, res, next) {
const Machine = this.models.get('ldap:Machine')
const machines = await Machine.find({active: true})
const data = []
for ( const machine of machines ) {
if ( !req.user.can(`ldap:machine:${machine.id}:view`) ) continue
data.push(await machine.to_api())
}
return res.api(data)
}
async get_machine_groups(req, res, next) {
const MachineGroup = this.models.get('ldap:MachineGroup')
const groups = await MachineGroup.find({active: true})
const data = []
for ( const group of groups ) {
if ( !req.user.can(`ldap:machine_group:${group.id}:view`) ) continue
data.push(await group.to_api())
}
return res.api(data)
}
async get_client(req, res, next) { async get_client(req, res, next) {
const Client = this.models.get('ldap:Client') const Client = this.models.get('ldap:Client')
const client = await Client.findById(req.params.id) const client = await Client.findById(req.params.id)
@ -80,6 +106,40 @@ class LDAPController extends Controller {
return res.api(await group.to_api()) return res.api(await group.to_api())
} }
async get_machine(req, res, next) {
const Machine = this.models.get('ldap:Machine')
const machine = await Machine.findById(req.params.id)
if ( !machine || !machine.active )
return res.status(404)
.message(req.T('api.machine_not_found'))
.api()
if ( !req.user.can(`ldap:machine:${machine.id}:view`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
return res.api(await machine.to_api())
}
async get_machine_group(req, res, next) {
const MachineGroup = this.models.get('ldap:MachineGroup')
const group = await MachineGroup.findById(req.params.id)
if ( !group || !group.active )
return res.status(404)
.message(req.T('api.group_not_found'))
.api()
if ( !req.user.can(`ldap:machine_group:${group.id}:view`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
return res.api(await group.to_api())
}
async create_client(req, res, next) { async create_client(req, res, next) {
if ( !req.user.can('ldap:client:create') ) if ( !req.user.can('ldap:client:create') )
return res.status(401) return res.status(401)
@ -96,7 +156,7 @@ class LDAPController extends Controller {
// Make sure the uid is free // Make sure the uid is free
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
const existing_user = await User.findOne({ uid: req.body.uid }) const existing_user = await User.findOne({ uid: req.body.uid.toLowerCase() })
if ( existing_user ) if ( existing_user )
return res.status(400) return res.status(400)
.message(req.T('api.user_already_exists')) .message(req.T('api.user_already_exists'))
@ -113,7 +173,7 @@ class LDAPController extends Controller {
// Create the client // Create the client
const Client = this.models.get('ldap:Client') const Client = this.models.get('ldap:Client')
const client = await Client.create({ const client = await Client.create({
uid: req.body.uid, uid: req.body.uid.toLowerCase(),
password: req.body.password, password: req.body.password,
name: req.body.name, name: req.body.name,
}) })
@ -121,13 +181,89 @@ class LDAPController extends Controller {
return res.api(await client.to_api()) return res.api(await client.to_api())
} }
async create_group(req, res, next) { async create_machine(req, res, next) {
console.log(req.body) // validate inputs
if ( !req.user.can(`ldap:group:create`) ) const required_fields = ['name', 'description']
return res.status(401) for ( const field of required_fields ) {
.message(req.T('api.insufficient_permissions')) if ( !req.body[field] )
return res.status(400)
.message(`${req.T('api.missing_field')} ${field}`)
.api()
}
// Make sure the machine name is free
const Machine = this.models.get('ldap:Machine')
const existing_machine = await Machine.findOne({ name: req.body.name })
if ( existing_machine )
return res.status(400)
.message(req.T('api.machine_already_exists'))
.api() .api()
const machine = new Machine({
name: req.body.name,
description: req.body.description,
host_name: req.body.host_name,
location: req.body.location,
})
if ( req.body.bind_password ) {
await machine.set_bind_password(req.body.bind_password)
}
if ( 'ldap_visible' in req.body ) {
machine.ldap_visible = !!req.body.ldap_visible
}
await machine.save()
return res.api(await machine.to_api())
}
async create_machine_group(req, res, next) {
// validate inputs
const required_fields = ['name']
for ( const field of required_fields ) {
if ( !req.body[field] )
return res.status(400)
.message(`${req.T('api.missing_field')} ${field}`)
.api()
}
// Make sure the machine name is free
const MachineGroup = this.models.get('ldap:MachineGroup')
const existing_group = await MachineGroup.findOne({ name: req.body.name })
if ( existing_group )
return res.status(400)
.message(req.T('api.group_already_exists'))
.api()
const group = new MachineGroup({
name: req.body.name,
description: req.body.description,
})
if ( 'ldap_visible' in req.body ) {
group.ldap_visible = !!req.body.ldap_visible
}
const Machine = this.models.get('ldap:Machine')
const machine_ids = Array.isArray(req.body.machine_ids) ? req.body.machine_ids : []
group.machine_ids = []
for ( const potential of machine_ids ) {
const machine = await Machine.findOne({
_id: Machine.to_object_id(potential),
active: true,
})
if ( machine ) {
group.machine_ids.push(potential)
}
}
await group.save()
return res.api(await group.to_api())
}
async create_group(req, res, next) {
// validate inputs // validate inputs
const required_fields = ['role', 'name'] const required_fields = ['role', 'name']
for ( const field of required_fields ) { for ( const field of required_fields ) {
@ -210,16 +346,16 @@ class LDAPController extends Controller {
} }
// Update the uid // Update the uid
if ( req.body.uid !== user.uid ) { if ( req.body.uid.toLowerCase() !== user.uid ) {
// Make sure the UID is free // Make sure the UID is free
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
const existing_user = await User.findOne({ uid: req.body.uid }) const existing_user = await User.findOne({ uid: req.body.uid.toLowerCase() })
if ( existing_user ) if ( existing_user )
return res.status(400) return res.status(400)
.message(req.T('api.user_already_exists')) .message(req.T('api.user_already_exists'))
.api() .api()
user.uid = req.body.uid user.uid = req.body.uid.toLowerCase()
} }
// Update the password // Update the password
@ -240,6 +376,106 @@ class LDAPController extends Controller {
return res.api() return res.api()
} }
async update_machine(req, res, next) {
const Machine = this.models.get('ldap:Machine')
const machine = await Machine.findById(req.params.id)
if ( !machine || !machine.active )
return res.status(404)
.message(req.T('api.machine_not_found'))
.api()
if ( !req.user.can(`ldap:machine:${machine.id}:update`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
const required_fields = ['name', 'description']
for ( const field of required_fields ) {
if ( !req.body[field] )
return res.status(400)
.message(`${req.T('api.missing_field')} ${field}`)
.api()
}
// Make sure the machine name is free
const existing_machine = await Machine.findOne({ name: req.body.name })
if ( existing_machine && existing_machine.id !== machine.id )
return res.status(400)
.message(req.T('api.machine_already_exists'))
.api()
machine.name = req.body.name
machine.description = req.body.description
machine.host_name = req.body.host_name
machine.location = req.body.location
if ( req.body.bind_password ) {
await machine.set_bind_password(req.body.bind_password)
}
if ( 'ldap_visible' in req.body ) {
machine.ldap_visible = !!req.body.ldap_visible
}
await machine.save()
return res.api(await machine.to_api())
}
async update_machine_group(req, res, next) {
const MachineGroup = this.models.get('ldap:MachineGroup')
const group = await MachineGroup.findById(req.params.id)
if ( !group || !group.active )
return res.status(404)
.message(req.T('api.group_not_found'))
.api()
if ( !req.user.can(`ldap:machine_group:${group.id}:update`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
const required_fields = ['name']
for ( const field of required_fields ) {
if ( !req.body[field] )
return res.status(400)
.message(`${req.T('api.missing_field')} ${field}`)
.api()
}
// Make sure the machine name is free
const existing_group = await MachineGroup.findOne({ name: req.body.name })
if ( existing_group && existing_group.id !== group.id )
return res.status(400)
.message(req.T('api.group_already_exists'))
.api()
group.name = req.body.name
group.description = req.body.description
if ( 'ldap_visible' in req.body ) {
group.ldap_visible = !!req.body.ldap_visible
}
const Machine = this.models.get('ldap:Machine')
const machine_ids = Array.isArray(req.body.machine_ids) ? req.body.machine_ids : []
group.machine_ids = []
for ( const potential of machine_ids ) {
const machine = await Machine.findOne({
_id: Machine.to_object_id(potential),
active: true,
})
if ( machine ) {
group.machine_ids.push(potential)
}
}
await group.save()
return res.api(await group.to_api())
}
async update_group(req, res, next) { async update_group(req, res, next) {
const User = await this.models.get('auth:User') const User = await this.models.get('auth:User')
const Group = await this.models.get('ldap:Group') const Group = await this.models.get('ldap:Group')
@ -337,6 +573,44 @@ class LDAPController extends Controller {
await group.save() await group.save()
return res.api() return res.api()
} }
async delete_machine(req, res, next) {
const Machine = this.models.get('ldap:Machine')
const machine = await Machine.findById(req.params.id)
if ( !machine || !machine.active )
return res.status(404)
.message(req.T('api.machine_not_found'))
.api()
if ( !req.user.can(`ldap:machine:${machine.id}:delete`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
machine.active = false
await machine.save()
return res.api()
}
async delete_machine_group(req, res, next) {
const MachineGroup = this.models.get('ldap:MachineGroup')
const group = await MachineGroup.findById(req.params.id)
if ( !group || !group.active )
return res.status(404)
.message(req.T('api.group_not_found'))
.api()
if ( !req.user.can(`ldap:machine_group:${group.id}:delete`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
group.active = false
await group.save()
return res.api()
}
} }
module.exports = exports = LDAPController module.exports = exports = LDAPController

View File

@ -20,6 +20,7 @@ class PasswordController extends Controller {
return { return {
created: x.created, created: x.created,
expires: x.expires, expires: x.expires,
accessed: x.accessed,
active: x.active, active: x.active,
name: x.name ?? req.T('common.unnamed'), name: x.name ?? req.T('common.unnamed'),
uuid: x.uuid, uuid: x.uuid,
@ -90,6 +91,10 @@ class PasswordController extends Controller {
await this.activity.password_reset({ req, ip: req.ip }) await this.activity.password_reset({ req, ip: req.ip })
if ( req.trap.has_trap() && req.trap.get_trap() === 'password_reset' ) await req.trap.end() if ( req.trap.has_trap() && req.trap.get_trap() === 'password_reset' ) await req.trap.end()
if ( req.session.registrant_flow ) {
await req.trap.begin('registrant_flow', { session_only: true })
}
// invalidate existing tokens and other logins // invalidate existing tokens and other logins
await req.user.logout(req) await req.user.logout(req)
await req.user.kickout() await req.user.kickout()

View File

@ -24,8 +24,9 @@ class ProfileController extends Controller {
last_name: user.last_name, last_name: user.last_name,
email: user.email, email: user.email,
uid: user.uid, uid: user.uid,
tagline: user.tagline, tagline: user.tagline || '',
user_id: user.id, user_id: user.id,
login_shell: user.login_shell || '',
...(user.notify_config ? { notify_config: await user.notify_config.to_api() } : {}) ...(user.notify_config ? { notify_config: await user.notify_config.to_api() } : {})
}) })
} }
@ -123,6 +124,8 @@ class ProfileController extends Controller {
async update(req, res, next) { async update(req, res, next) {
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
const Message = this.models.get('Message')
const Setting = this.models.get('Setting')
let user let user
if ( req.params.user_id === 'me' ) user = req.user if ( req.params.user_id === 'me' ) user = req.user
@ -154,14 +157,22 @@ class ProfileController extends Controller {
.api() .api()
// Update the user's profile // Update the user's profile
if ( user.email !== req.body.email && (await Setting.get('auth.require_email_verify')) ) {
await req.trap.begin('verify_email', { session_only: false })
await Message.create(req.user, 'Your e-mail address has changed, and a verification e-mail has been sent. You must complete this process to continue.')
}
user.first_name = req.body.first_name user.first_name = req.body.first_name
user.last_name = req.body.last_name user.last_name = req.body.last_name
user.email = req.body.email user.email = req.body.email
user.tagline = req.body.tagline user.tagline = req.body.tagline
user.login_shell = req.body.login_shell
// Save the record // Save the record
await user.save() await user.save()
return res.api() return res.api({
force_message_refresh: true,
})
} }
async update_photo(req, res, next) { async update_photo(req, res, next) {

View File

@ -0,0 +1,195 @@
const { Controller } = require('libflitter')
class RadiusController extends Controller {
static get services() {
return [...super.services, 'models', 'output']
}
async attempt(req, res, next) {
const User = this.models.get('auth:User')
const Client = this.models.get('radius:Client')
this.output.debug('RADIUS attempt:')
this.output.debug(req.body)
if ( !req.body.username || !req.body.password ) {
this.output.error('RADIUS error: missing username or password')
return this.fail(res)
}
const parts = String(req.body.username).split('@')
parts.reverse()
const clientId = parts.shift()
parts.reverse()
const username = parts.join('@')
const password = String(req.body.password).replace(/\0/g, '')
this.output.debug(`clientId: ${clientId}, username: ${username}, password: ${password}`)
const user = await User.findOne({ uid: username, active: true })
if ( !user ) {
this.output.error(`RADIUS error: invalid username: ${username}`)
return this.fail(res)
}
const client = await Client.findById(clientId)
if ( !client || !client.active ) {
this.output.error(`RADIUS error: invalid client: ${clientId}`)
return this.fail(res)
}
// Check if the credentials are an app_password
const app_password_verified = Array.isArray(user.app_passwords)
&& user.app_passwords.length > 0
&& await user.check_app_password(password)
// Check if the user has MFA enabled.
// If so, split the incoming password to fetch the MFA code
// e.g. normalPassword:123456
if ( !app_password_verified && user.mfa_enabled ) {
const parts = password.split(':')
const mfa_code = parts.pop()
const actual_password = parts.join(':')
// Check the credentials
if ( !(await user.check_password(actual_password)) ) {
this.output.debug(`RADIUS error: user w/ MFA provided invalid credentials`)
return this.fail(res)
}
// Now, check the MFA code
if ( !user.mfa_token.verify(mfa_code) ) {
this.output.debug(`RADIUS error: user w/ MFA provided invalid MFA token`)
return this.fail(res)
}
// If not MFA, just check the credentials
} else if (!app_password_verified && !await user.check_password(password)) {
this.output.debug(`RADIUS error: user w/ simple auth provided invalid credentials`)
return this.fail(res)
}
// Check if the user has any login interrupt traps set
if ( user.trap ) {
this.output.error(`RADIUS error: user has trap: ${user.trap}`)
return this.fail(res)
}
// Apply the appropriate IAM policy if this SAML SP is associated with an App
// If the SAML service provider has no associated application, just allow it
const associated_app = await client.application()
if ( associated_app ) {
const Policy = this.models.get('iam:Policy')
const can_access = await Policy.check_user_access(user, associated_app.id)
if ( !can_access ) {
this.output.error(`RADIUS error: user denied IAM access`)
return this.fail(res)
}
}
this.output.info(`Authenticated RADIUS user: ${user.uid} to IAM ${associated_app.name}`)
return res.api({ success: true })
}
fail(res) {
return res.status(401).api({ success: false })
}
async get_clients(req, res, next) {
const Client = this.models.get('radius:Client')
const clients = await Client.find({ active: true })
const data = []
for ( const client of clients ) {
if ( req.user.can(`radius:client:${client.id}:view`) ) {
data.push(await client.to_api())
}
}
return res.api(data)
}
async get_client(req, res, next) {
const Client = this.models.get('radius:Client')
const client = await Client.findById(req.params.id)
if ( !client || !client.active )
return res.status(404)
.message(req.T('api.client_not_found'))
.api()
if ( !req.user.can(`radius:client:${client.id}:view`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
return res.api(await client.to_api())
}
async create_client(req, res, next) {
if ( !req.user.can('radius:client:create') )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
if ( !req.body.name )
return res.status(400)
.message(`${req.T('api.missing_field')} name`)
.api()
const Client = this.models.get('radius:Client')
const client = new Client({
name: req.body.name,
})
await client.save()
return res.api(await client.to_api())
}
async update_client(req, res, next) {
const Client = this.models.get('radius:Client')
const client = await Client.findById(req.params.id)
if ( !client || !client.active )
return res.status(404)
.message(req.T('api.client_not_found'))
.api()
if ( !req.user.can(`radius:client:${client.id}:update`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
if ( !req.body.name )
return res.status(400)
.message(`${req.T('api.missing_field')} name`)
.api()
client.name = req.body.name
await client.save()
return res.api()
}
async delete_client(req, res, next) {
const Client = this.models.get('radius:Client')
const client = await Client.findById(req.params.id)
if ( !client || !client.active )
return res.status(404)
.message(req.T('api.client_not_found'))
.api()
if ( !req.user.can(`radius:client:${client.id}:delete`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
.api()
client.active = false
await client.save()
return res.api()
}
}
module.exports = exports = RadiusController

View File

@ -7,15 +7,69 @@ const FormController = require('flitter-auth/controllers/Forms')
*/ */
class Forms extends FormController { class Forms extends FormController {
static get services() { static get services() {
return [...super.services, 'Vue', 'models'] return [...super.services, 'Vue', 'models', 'jobs']
} }
async registration_provider_get(req, res, next) { async registration_provider_get(req, res, next) {
if ( req.session.auth.flow ) {
req.session.registrant_flow = req.session.auth.flow
}
return res.page('auth:register', { return res.page('auth:register', {
...this.Vue.data({}) ...this.Vue.data({})
}) })
} }
async email_verify_keyaction(req, res, next) {
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
req.user.email_verified = true
await req.user.save()
await req.trap.end()
const url = req.session.email_verify_flow || '/dash/profile'
return res.redirect(url)
}
async show_verify_email(req, res, next) {
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
const verify_queue = this.jobs.queue('verifications')
await verify_queue.add('SendVerificationEmail', { user_id: req.user.id })
return res.page('public:message', {
...this.Vue.data({
message: req.T('auth.must_verify_email'),
actions: [
{
text: 'Send Verification E-Mail',
action: 'redirect',
next: '/auth/verify-email/sent',
},
],
})
})
}
async send_verify_email(req, res, next) {
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
return res.page('public:message', {
...this.Vue.data({
message: req.T('auth.verify_email_sent'),
actions: [
{
text: 'Re-send Verification E-Mail',
action: 'redirect',
next: '/auth/verify-email/sent',
},
],
})
})
}
async finish_registration(req, res, next) {
if ( req.trap.has_trap() && req.trap.get_trap() === 'registrant_flow' ) await req.trap.end()
const dest = req.session.registrant_flow || '/dash/profile'
return res.redirect(dest)
}
async login_provider_get(req, res, next) { async login_provider_get(req, res, next) {
const Setting = this.models.get('Setting') const Setting = this.models.get('Setting')

View File

@ -8,7 +8,7 @@ const Oauth2Controller = require('flitter-auth/controllers/Oauth2')
*/ */
class Oauth2 extends Oauth2Controller { class Oauth2 extends Oauth2Controller {
static get services() { static get services() {
return [...super.services, 'Vue', 'configs', 'models'] return [...super.services, 'Vue', 'configs', 'models', 'output']
} }
async authorize_post(req, res, next) { async authorize_post(req, res, next) {
@ -18,6 +18,24 @@ class Oauth2 extends Oauth2Controller {
const StarshipClient = this.models.get('oauth:Client') const StarshipClient = this.models.get('oauth:Client')
const starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID }) const starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID })
// Make sure the user has IAM access before proceeding
const Application = this.models.get('Application')
const Policy = this.models.get('iam:Policy')
const application = await Application.findOne({ oauth_client_ids: starship_client.id })
if ( !application ) {
this.output.warn(`IAM Denial: OAuth client not associated with an application: ${starship_client.id}`)
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash',
})
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
this.output.warn(`IAM Denial: User ${req.user.uid} not authorized to access application: ${application.id}`)
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash',
})
}
req.user.authorize(starship_client) req.user.authorize(starship_client)
await req.user.save() await req.user.save()
return super.authorize_post(req, res, next) return super.authorize_post(req, res, next)
@ -26,11 +44,35 @@ class Oauth2 extends Oauth2Controller {
async authorize_get(req, res, next) { async authorize_get(req, res, next) {
const client = await this._get_authorize_client(req) const client = await this._get_authorize_client(req)
if ( !client ) return this._uniform(res, req.T('auth.unable_to_authorize')) if ( !client ) return this._uniform(res, req.T('auth.unable_to_authorize'))
const uri = new URL(req.query.redirect_uri) const uri = new URL(Array.isArray(req.query.redirect_uri) ? req.query.redirect_uri[0] : req.query.redirect_uri)
const StarshipClient = this.models.get('oauth:Client') const StarshipClient = this.models.get('oauth:Client')
const starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID }) const starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID })
// Make sure the user has IAM access before proceeding
const Application = this.models.get('Application')
const Policy = this.models.get('iam:Policy')
const application = await Application.findOne({ oauth_client_ids: starship_client.id })
if ( !application ) {
this.output.warn(`IAM Denial: OAuth client not associated with an application: ${starship_client.id}`)
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash',
})
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
this.output.warn(`IAM Denial: User ${req.user.uid} not authorized to access application: ${application.id}`)
return this.Vue.auth_message(res, {
message: req.T('saml.no_access').replace('APP_NAME', application.name),
next_destination: '/dash',
})
}
let state;
if ( state = (req.query.state || req.body.state) ) {
state = Array.isArray(state) ? state[0] : state
uri.searchParams.set('state', state)
}
if ( req.user.has_authorized(starship_client) ) { if ( req.user.has_authorized(starship_client) ) {
return this.Vue.invoke_action(res, { return this.Vue.invoke_action(res, {
text: 'Grant Access', text: 'Grant Access',

View File

@ -67,7 +67,7 @@ class SAMLController extends Controller {
key: await this.saml.private_key(), key: await this.saml.private_key(),
protocolBinding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', protocolBinding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
clearIdPSession: done => { clearIdPSession: done => {
this.output.info(`${req.T('saml.clear_idp_session')} ${req.user.uid}`) this.output.info(`${req.T('saml.clear_idp_session')} ${req.user.uid.toLowerCase()}`)
req.saml.participants.clear().then(async () => { req.saml.participants.clear().then(async () => {
if ( this.saml.config().slo.end_coreid_session ) { if ( this.saml.config().slo.end_coreid_session ) {
await req.user.logout(req) await req.user.logout(req)

View File

@ -12,7 +12,7 @@ class EMailJob extends Job {
const { data } = job const { data } = job
let { from = config.default_sender, to, subject, html = undefined, email_params = undefined } = data let { from = config.default_sender, to, subject, html = undefined, email_params = undefined } = data
this.output.info(`Sending mail to ${to}...`) this.info(`Sending mail to ${to}...`)
if ( !html && email_params ) html = this.email(email_params) if ( !html && email_params ) html = this.email(email_params)
@ -20,9 +20,11 @@ class EMailJob extends Job {
from, to, subject, html, from, to, subject, html,
}) })
} catch (e) { } catch (e) {
this.output.error(e) this.error(e)
throw e
} }
this.output.success(`Mail sent!`)
this.success(`Mail sent!`)
} }
email({ header_text, body_paragraphs = [], button_text = '', button_link = '' }) { email({ header_text, body_paragraphs = [], button_text = '', button_link = '' }) {

View File

@ -12,7 +12,7 @@ class ForeignIPLoginAlertJob extends Job {
const user = await User.findById(user_id) const user = await User.findById(user_id)
if ( !user ) throw new Error('Unable to find user with ID: '+user_id) if ( !user ) throw new Error('Unable to find user with ID: '+user_id)
this.output.info('Sending foreign IP login alert to user.') this.info('Sending foreign IP login alert to user ' + user.uid)
await this.jobs.queue('mailer').add('EMail', { await this.jobs.queue('mailer').add('EMail', {
to: user.email, to: user.email,
@ -29,14 +29,19 @@ class ForeignIPLoginAlertJob extends Job {
} }
}) })
this.info('Logged e-mail job')
if ( user.notify_config && user.notify_config.active ) { if ( user.notify_config && user.notify_config.active ) {
await user.notify_config.log({ await user.notify_config.log({
title: `${this.configs.get('app.name')}: Sign-In From New IP`, title: `${this.configs.get('app.name')}: Sign-In From New IP`,
message: `Someone signed into your account (${user.uid}) from the IP address ${ip}. If this was you, no further action is required.`, message: `Someone signed into your account (${user.uid}) from the IP address ${ip}. If this was you, no further action is required.`,
}) })
this.info('Logged push notification job')
} }
} catch (e) { } catch (e) {
this.output.error(e) this.error(e)
throw e
} }
} }
} }

View File

@ -13,13 +13,17 @@ class PasswordResetJob extends Job {
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
const user = await User.findById(user_id) const user = await User.findById(user_id)
if (!user) { if (!user) {
this.output.error(`Unable to find user with ID: ${user_id}`) this.error(`Unable to find user with ID: ${user_id}`)
throw new Error('Unable to find user with that ID.') throw new Error('Unable to find user with that ID.')
} }
this.output.info(`Resetting password for user: ${user.uid}`) this.info(`Resetting password for user: ${user.uid}`)
// Create an authenticated key-action // Create an authenticated key-action
const key_action = await this.key_action(user) const key_action = await this.key_action(user)
this.info(`Created reset keyaction ${key_action.id} (key: ${key_action.key}, handler: ${key_action.handler})`)
await this.jobs.queue('mailer').add('EMail', { await this.jobs.queue('mailer').add('EMail', {
to: user.email, to: user.email,
subject: 'Reset Your Password | ' + this.configs.get('app.name'), subject: 'Reset Your Password | ' + this.configs.get('app.name'),
@ -34,17 +38,22 @@ class PasswordResetJob extends Job {
} }
}) })
this.info('Logged e-mail job.')
if ( user.notify_config && user.notify_config.active ) { if ( user.notify_config && user.notify_config.active ) {
await user.notify_config.log({ await user.notify_config.log({
title: `${this.configs.get('app.name')}: Password Reset Requested`, title: `${this.configs.get('app.name')}: Password Reset Requested`,
message: `A password reset request was logged for your account (${user.uid}). If this was you, please check your e-mail for further instructions.`, message: `A password reset request was logged for your account (${user.uid}). If this was you, please check your e-mail for further instructions.`,
priority: 8, priority: 8,
}) })
this.info('Logged security push notification job')
} }
this.output.success('Password reset logged.') this.success('Password reset logged.')
} catch (e) { } catch (e) {
this.output.error(e) this.error(e)
throw e
} }
} }

View File

@ -14,7 +14,7 @@ class PasswordResetAlertJob extends Job {
const user = await User.findById(user_id) const user = await User.findById(user_id)
if ( !user ) throw new Error('Unable to find user with ID: '+user_id) if ( !user ) throw new Error('Unable to find user with ID: '+user_id)
this.output.info('Sending password reset alert to user.') this.info('Sending password reset alert to user ' + user.uid)
await this.jobs.queue('mailer').add('EMail', { await this.jobs.queue('mailer').add('EMail', {
to: user.email, to: user.email,
@ -28,15 +28,20 @@ class PasswordResetAlertJob extends Job {
}, },
}) })
this.info('Logged e-mail job')
if ( user.notify_config && user.notify_config.active ) { if ( user.notify_config && user.notify_config.active ) {
await user.notify_config.log({ await user.notify_config.log({
title: `${this.configs.get('app.name')}: Password Reset`, title: `${this.configs.get('app.name')}: Password Reset`,
message: `The password to your account (${user.uid}) was reset from the IP address ${ip}. If this was not you, please contact your system administrator.`, message: `The password to your account (${user.uid}) was reset from the IP address ${ip}. If this was not you, please contact your system administrator.`,
priority: 8, priority: 8,
}) })
this.info('Logged push notification job')
} }
} catch (e) { } catch (e) {
this.output.error(e) this.error(e)
throw e
} }
} }
} }

View File

@ -14,14 +14,15 @@ class PopulateAnnouncementJob extends Job {
const announcement = await Announcement.findById(announcement_id) const announcement = await Announcement.findById(announcement_id)
if ( !announcement ) { if ( !announcement ) {
this.output.error(`Unable to find announcement with ID: ${announcement_id}`) this.error(`Unable to find announcement with ID: ${announcement_id}`)
throw new Error('Unable to find announcement with that ID.') throw new Error('Unable to find announcement with that ID.')
} }
await announcement.populate() await announcement.populate()
this.output.success('Populated announcements.') this.success('Populated announcements.')
} catch (e) { } catch (e) {
this.output.error(e) this.error(e)
throw e
} }
} }
} }

View File

@ -18,13 +18,15 @@ class PushNotifyJob extends Job {
const notify = user.notify_config const notify = user.notify_config
if ( !notify || !notify.active ) throw new Error('User does not have notifications configured.') if ( !notify || !notify.active ) throw new Error('User does not have notifications configured.')
this.output.info(`Sending notification to ${user.uid}...`) this.info(`Sending notification to ${user.uid}...`)
await notify.send({ title, message, priority }) await notify.send({ title, message, priority })
} catch (e) { } catch (e) {
this.output.error(e) this.error(e)
throw e
} }
this.output.success(`Notification sent!`)
this.success(`Notification sent!`)
} }
} }

View File

@ -0,0 +1,62 @@
const { Job } = require('flitter-jobs')
class SendVerificationEmailJob extends Job {
static get services() {
return [...super.services, 'models', 'jobs', 'output', 'configs']
}
async execute(job) {
const {data} = job
const {user_id} = data
try {
const User = this.models.get('auth:User')
const user = await User.findById(user_id)
if (!user) {
this.error(`Unable to find user with ID: ${user_id}`)
throw new Error('Unable to find user with that ID.')
}
this.info(`Sending verification email for user: ${user.uid}`)
// Create an authenticated key-action
const key_action = await this.key_action(user)
this.info(`Created verification keyaction ${key_action.id} (key: ${key_action.key}, handler: ${key_action.handler})`)
await this.jobs.queue('mailer').add('EMail', {
to: user.email,
subject: 'Confirm Your E-mail | ' + this.configs.get('app.name'),
email_params: {
header_text: 'Confirm Your E-mail',
body_paragraphs: [
'The e-mail address for your ' + this.configs.get('app.name') + ' was set or changed. Click the link below to verify this change.',
'If you didn\'t request this e-mail, please contact your system administrator.',
],
button_text: 'Confirm E-mail',
button_link: key_action.url(),
}
})
this.info('Logged e-mail job.')
} catch (e) {
this.error(e)
throw e
}
}
async key_action(user) {
const KeyAction = this.models.get('auth:KeyAction')
const ka_data = {
handler: 'controller::auth:Forms.email_verify_keyaction',
used: false,
user_id: user._id,
auto_login: true,
no_auto_logout: false,
}
return (new KeyAction(ka_data)).save()
}
}
module.exports = exports = SendVerificationEmailJob

View File

@ -1,4 +1,5 @@
const LDAPController = require('./LDAPController') const LDAPController = require('./LDAPController')
const LDAP = require('ldapjs')
class GroupsController extends LDAPController { class GroupsController extends LDAPController {
static get services() { static get services() {

View File

@ -50,7 +50,7 @@ class LDAPController extends Injectable {
const item = await this.get_resource_from_dn(req.dn) const item = await this.get_resource_from_dn(req.dn)
if ( !item ) { if ( !item ) {
this.output.debug(`Bind failure: ${req.dn} not found`) this.output.debug(`Bind failure: ${req.dn} not found`)
return next(new LDAP.NoSuchObject()) return next(new LDAP.NoSuchObjectError())
} }
// If the object is can-able, make sure it can bind // If the object is can-able, make sure it can bind
@ -59,34 +59,8 @@ class LDAPController extends Injectable {
return next(new LDAP.InsufficientAccessRightsError()) return next(new LDAP.InsufficientAccessRightsError())
} }
// Check if the credentials are an app_password // Check if the credentials are valid
const app_password_verified = Array.isArray(item.app_passwords) if ( !(await item.check_credential_string(req.credentials)) ) {
&& item.app_passwords.length > 0
&& await item.check_app_password(req.credentials)
// Check if the user has MFA enabled.
// If so, split the incoming password to fetch the MFA code
// e.g. normalPassword:123456
if ( !app_password_verified && item.mfa_enabled ) {
const parts = req.credentials.split(':')
const mfa_code = parts.pop()
const actual_password = parts.join(':')
// Check the credentials
if ( !await item.check_password(actual_password) ) {
this.output.debug(`Bind failure: user w/ MFA provided invalid credentials`)
return next(new LDAP.InvalidCredentialsError('Invalid credentials. Make sure MFA code is included at the end of your password (e.g. password:123456)'))
}
// Now, check the MFA code
if ( !item.mfa_token.verify(mfa_code) ) {
this.output.debug(`Bind failure: user w/ MFA provided invalid MFA token`)
return next(new LDAP.InvalidCredentialsError('Invalid credentials. Verification of the MFA token failed.'))
}
// If not MFA, just check the credentials
} else if (!app_password_verified && !await item.check_password(req.credentials)) {
this.output.debug(`Bind failure: user w/ simple auth provided invalid credentials`)
return next(new LDAP.InvalidCredentialsError()) return next(new LDAP.InvalidCredentialsError())
} }

View File

@ -0,0 +1,146 @@
const LDAPController = require('./LDAPController')
const LDAP = require('ldapjs')
class SudoController extends LDAPController {
static get services() {
return [
...super.services,
'output',
'ldap_server',
'models',
'configs',
'auth'
]
}
constructor() {
super()
this.Group = this.models.get('auth:Group')
this.User = this.models.get('auth:User')
}
// TODO flitter-orm chunk query
// TODO generalize scoped search logic
async search_sudo(req, res, next) {
if ( !req.user.can('ldap:search:sudo') ) {
return next(new LDAP.InsufficientAccessRightsError())
}
const sudo_hosts = this.parse_sudo_hosts(req.filter)
const iam_targets = await this.get_targets_from_hosts(sudo_hosts)
if ( req.scope === 'base' ) {
// If scope is base, check if the base DN matches the filter.
// If so, return it. Else, return empty.
this.output.debug(`Running base DN search for sudo with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
const user = await this.get_resource_from_dn(req.dn)
// Make sure the user is ldap visible && match the filter
if ( user && user.ldap_visible && req.filter.matches(await user.to_sudo(iam_targets)) ) {
// If so, send the object
res.send({
dn: user.sudo_dn.format(this.configs.get('ldap:server.format')),
attributes: await user.to_sudo(iam_targets),
})
}
} else if ( req.scope === 'one' ) {
// If scope is one, find all entries that are the immediate
// subordinates of the base DN that match the filter.
this.output.debug(`Running one DN search for sudo with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
// Fetch the LDAP-visible users
const users = await this.Group.sudo_directory()
for ( const user of users ) {
// Make sure the user os of the appropriate scope
if ( req.dn.equals(user.sudo_dn) || user.sudo_dn.parent().equals(req.dn) ) {
// Check if the filter matches
if ( req.filter.matches(await user.to_sudo(iam_targets)) ) {
// If so, send the object
res.send({
dn: user.sudo_dn.format(this.configs.get('ldap:server.format')),
attributes: await user.to_sudo(iam_targets),
})
}
}
}
} else if ( req.scope === 'sub' ) {
// If scope is sub, find all entries that are subordinates
// of the base DN at any level and match the filter.
this.output.debug(`Running sub DN search for sudo with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`)
// Fetch the users as LDAP objects
const users = await this.Group.sudo_directory()
for ( const user of users ) {
// Make sure the user is of appropriate scope
if ( req.dn.equals(user.sudo_dn) || req.dn.parentOf(user.sudo_dn) ) {
// Check if filter matches
if ( req.filter.matches(await user.to_sudo(iam_targets)) ) {
// If so, send the object
res.send({
dn: user.sudo_dn.format(this.configs.get('ldap:server.format')),
attributes: await user.to_sudo(iam_targets),
})
}
}
}
} else {
this.output.error(`Attempted to perform LDAP search with invalid scope: ${req.scope}`)
return next(new LDAP.OtherError('Attempted to perform LDAP search with invalid scope.'))
}
res.end()
return next()
}
parse_sudo_hosts(filter, target_hosts = []) {
if ( Array.isArray(filter?.filters) ) {
for ( const sub_filter of filter.filters ) {
target_hosts = [...target_hosts, ...this.parse_sudo_hosts(sub_filter)]
}
} else if ( filter?.attribute ) {
if ( filter.attribute === 'sudohost' ) {
target_hosts.push(filter.value)
}
}
return target_hosts.filter(Boolean)
}
async get_targets_from_hosts(sudo_hosts) {
const Machine = this.models.get('ldap:Machine')
const machines = await Machine.find({
active: true,
ldap_visible: true,
host_name: {
$in: sudo_hosts.filter(x => x.toLowerCase() !== 'all' && x.indexOf('*') < 0),
}
})
return machines.map(x => x.id)
}
get_cn_from_dn(dn) {
try {
if ( typeof dn === 'string' ) dn = LDAP.parseDN(dn)
return dn.rdns[0].attrs.cn.value
} catch (e) { console.log('Error parsing CN from DN', e) }
}
async get_resource_from_dn(sudo_dn) {
const cn = this.get_cn_from_dn(sudo_dn)
if ( cn ) {
return this.User.findOne({uid: cn.substr(5), ldap_visible: true})
}
}
}
module.exports = exports = SudoController

View File

@ -52,7 +52,7 @@ class UsersController extends LDAPController {
first_name: req_data.cn ? req_data.cn[0] : '', first_name: req_data.cn ? req_data.cn[0] : '',
last_name: req_data.sn ? req_data.sn[0] : '', last_name: req_data.sn ? req_data.sn[0] : '',
email: req_data.mail ? req_data.mail[0] : '', email: req_data.mail ? req_data.mail[0] : '',
username: req_data.uid ? req_data.uid[0] : '', username: req_data.uid ? req_data.uid[0].toLowerCase() : '',
password: req_data.userpassword ? req_data.userpassword[0] : '', password: req_data.userpassword ? req_data.userpassword[0] : '',
} }
@ -218,10 +218,12 @@ class UsersController extends LDAPController {
// TODO flitter-orm chunk query // TODO flitter-orm chunk query
// TODO generalize scoped search logic // TODO generalize scoped search logic
async search_people(req, res, next) { async search_people(req, res, next) {
if ( !req.user.can('ldap:search:users') ) { if ( !req.user.can('ldap:search:users:me') ) {
return next(new LDAP.InsufficientAccessRightsError()) return next(new LDAP.InsufficientAccessRightsError())
} }
const can_search_all = req.user.can('ldap:search:users')
const iam_targets = this.parse_iam_targets(req.filter) const iam_targets = this.parse_iam_targets(req.filter)
if ( req.scope === 'base' ) { if ( req.scope === 'base' ) {
// If scope is base, check if the base DN matches the filter. // If scope is base, check if the base DN matches the filter.
@ -231,7 +233,12 @@ class UsersController extends LDAPController {
const user = await this.get_resource_from_dn(req.dn) const user = await this.get_resource_from_dn(req.dn)
// Make sure the user is ldap visible && match the filter // Make sure the user is ldap visible && match the filter
if ( user && user.ldap_visible && req.filter.matches(await user.to_ldap(iam_targets)) ) { if (
user
&& user.ldap_visible
&& req.filter.matches(await user.to_ldap(iam_targets))
&& (req.user.id === user.id || can_search_all)
) {
// If so, send the object // If so, send the object
res.send({ res.send({
@ -255,6 +262,7 @@ class UsersController extends LDAPController {
// Fetch the LDAP-visible users // Fetch the LDAP-visible users
const users = await this.User.ldap_directory() const users = await this.User.ldap_directory()
for ( const user of users ) { for ( const user of users ) {
if ( user.id !== req.user.id && !can_search_all ) continue
// Make sure the user os of the appropriate scope // Make sure the user os of the appropriate scope
if ( req.dn.equals(user.dn) || user.dn.parent().equals(req.dn) ) { if ( req.dn.equals(user.dn) || user.dn.parent().equals(req.dn) ) {
@ -283,6 +291,7 @@ class UsersController extends LDAPController {
this.output.debug(`Filter:`) this.output.debug(`Filter:`)
this.output.debug(this.filter_to_obj(req.filter.json)) this.output.debug(this.filter_to_obj(req.filter.json))
for ( const user of users ) { for ( const user of users ) {
if ( user.id !== req.user.id && !can_search_all ) continue
this.output.debug(`Checking ${user.uid}...`) this.output.debug(`Checking ${user.uid}...`)
this.output.debug(`DN: ${user.dn}`) this.output.debug(`DN: ${user.dn}`)
this.output.debug(`Req DN equals: ${req.dn.equals(user.dn)}`) this.output.debug(`Req DN equals: ${req.dn.equals(user.dn)}`)
@ -290,6 +299,7 @@ class UsersController extends LDAPController {
// Make sure the user is of appropriate scope // Make sure the user is of appropriate scope
if ( req.dn.equals(user.dn) || req.dn.parentOf(user.dn) ) { if ( req.dn.equals(user.dn) || req.dn.parentOf(user.dn) ) {
this.output.debug(await user.to_ldap())
this.output.debug(`Matches sub scope. Matches filter? ${req.filter.matches(await user.to_ldap(iam_targets))}`) this.output.debug(`Matches sub scope. Matches filter? ${req.filter.matches(await user.to_ldap(iam_targets))}`)
// Check if filter matches // Check if filter matches
@ -317,7 +327,7 @@ class UsersController extends LDAPController {
try { try {
if ( typeof dn === 'string' ) dn = LDAP.parseDN(dn) if ( typeof dn === 'string' ) dn = LDAP.parseDN(dn)
return dn.rdns[0].attrs[uid_field].value return dn.rdns[0].attrs[uid_field].value.toLowerCase()
} catch (e) {} } catch (e) {}
} }
@ -325,7 +335,7 @@ class UsersController extends LDAPController {
const uid = this.get_uid_from_dn(dn) const uid = this.get_uid_from_dn(dn)
if ( uid ) { if ( uid ) {
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
return User.findOne({uid, ldap_visible: true}) return User.findOne({uid: uid.toLowerCase(), ldap_visible: true})
} }
} }
} }

View File

@ -0,0 +1,28 @@
const sudo_routes = {
prefix: false, // false | string
middleware: [
'Logger'
],
search: {
'ou=sudo': [
'ldap_middleware::BindUser',
'ldap_controller::Sudo.search_sudo',
],
},
bind: {},
add: {},
del: {},
modify: {},
compare: {},
}
module.exports = exports = sudo_routes

View File

@ -11,6 +11,7 @@ class ApplicationModel extends Model {
ldap_client_ids: [String], ldap_client_ids: [String],
oauth_client_ids: [String], oauth_client_ids: [String],
openid_client_ids: [String], openid_client_ids: [String],
radius_client_ids: [String],
} }
} }
@ -24,6 +25,7 @@ class ApplicationModel extends Model {
ldap_client_ids: this.ldap_client_ids, ldap_client_ids: this.ldap_client_ids,
oauth_client_ids: this.oauth_client_ids, oauth_client_ids: this.oauth_client_ids,
openid_client_ids: this.openid_client_ids, openid_client_ids: this.openid_client_ids,
radius_client_ids: this.radius_client_ids || [],
} }
} }
} }

View File

@ -0,0 +1,29 @@
const { Model } = require('flitter-orm')
class FrontEndErrorModel extends Model {
static get schema() {
return {
user_agent: String,
logged_at: { type: Date, default: () => new Date },
user_id: String,
session_id: String,
full_url: String,
trace: String,
}
}
static async log(request) {
const err = new this({
user_agent: request.get('user-agent'),
user_id: request?.user?.id,
session_id: request.sessionID,
full_url: request.body.full_url,
trace: request.body.trace,
})
await err.save()
return err
}
}
module.exports = exports = FrontEndErrorModel

View File

@ -7,6 +7,7 @@ class AppPasswordModel extends Model {
return { return {
hash: String, hash: String,
created: { type: Date, default: () => new Date }, created: { type: Date, default: () => new Date },
accessed: Date,
expires: Date, expires: Date,
active: { type: Boolean, default: true }, active: { type: Boolean, default: true },
name: String, name: String,

View File

@ -11,6 +11,9 @@ class GroupModel extends Model {
return { return {
name: String, name: String,
user_ids: [String], user_ids: [String],
posix_user_id: String,
posix_group_id: Number,
grants_sudo: { type: Boolean, default: false },
active: { type: Boolean, default: true }, active: { type: Boolean, default: true },
ldap_visible: { type: Boolean, default: true }, ldap_visible: { type: Boolean, default: true },
} }
@ -29,18 +32,72 @@ class GroupModel extends Model {
return await User.find({ _id: { $in: this.user_ids.map(x => this.constructor.to_object_id(x)) } }) return await User.find({ _id: { $in: this.user_ids.map(x => this.constructor.to_object_id(x)) } })
} }
async get_gid_number() {
if ( !this.posix_group_id ) {
const Setting = this.models.get('Setting')
let last_uid = await Setting.get('ldap.last_alloc_uid')
if ( last_uid < 1 ) {
last_uid = this.configs.get('ldap:server.schema.start_uid')
}
this.posix_group_id = last_uid + 1
await Setting.set('ldap.last_alloc_uid', this.posix_group_id)
await this.save()
}
return this.posix_group_id
}
async to_ldap() { async to_ldap() {
const users = await this.users() const users = await this.users()
return { return {
cn: this.name, cn: this.name,
dn: this.dn.format(this.configs.get('ldap:server.format')), dn: this.dn.format(this.configs.get('ldap:server.format')),
objectClass: 'groupOfNames', objectClass: ['groupOfNames', 'posixGroup'],
gidNumber: String(await this.get_gid_number()),
member: users.map(x => x.dn.format(this.configs.get('ldap:server.format'))), member: users.map(x => x.dn.format(this.configs.get('ldap:server.format'))),
} }
} }
static async sudo_directory() {
const groups = await this.find({ ldap_visible: true, active: true, grants_sudo: true })
let users = []
for ( const group of groups ) {
users = [...users, ...(await group.users())]
}
return users.filter(u => u.uid !== 'root')
}
static async ldap_directory() { static async ldap_directory() {
return this.find({ ldap_visible: true, active: true }) const User = this.prototype.models.get('auth:User')
const groups = await this.find({ ldap_visible: true, active: true })
const posix_user_ids = groups.map(group => group.posix_user_id)
.filter(Boolean)
.map(id => User.to_object_id(id))
const missing_posix_users = await User.find({
ldap_visible: true,
_id: {
$nin: posix_user_ids
}
})
for ( const user of missing_posix_users ) {
const group = new this({
name: `${user.uid} (posix)`,
user_ids: [user.id],
posix_user_id: user.id,
posix_group_id: await user.get_uid_number(),
})
await group.save()
groups.push(group)
}
return groups
} }
async to_api() { async to_api() {
@ -49,6 +106,7 @@ class GroupModel extends Model {
name: this.name, name: this.name,
user_ids: this.user_ids, user_ids: this.user_ids,
ldap_visible: this.ldap_visible, ldap_visible: this.ldap_visible,
grants_sudo: !!this.grants_sudo,
} }
} }
} }

View File

@ -26,6 +26,7 @@ class User extends AuthUser {
last_name: String, last_name: String,
tagline: String, tagline: String,
email: String, email: String,
email_verified: {type: Boolean, default: false},
ldap_visible: {type: Boolean, default: true}, ldap_visible: {type: Boolean, default: true},
active: {type: Boolean, default: true}, active: {type: Boolean, default: true},
mfa_token: MFAToken, mfa_token: MFAToken,
@ -38,9 +39,42 @@ class User extends AuthUser {
photo_file_id: String, photo_file_id: String,
trap: String, trap: String,
notify_config: NotifyConfig, notify_config: NotifyConfig,
uid_number: Number,
login_shell: String,
is_default_user_for_coreid: { type: Boolean, default: false },
}} }}
} }
async grant_defaults() {
const default_user = await this.constructor.findOne({is_default_user_for_coreid: true, active: true})
this.login_shell = default_user.login_shell
this.roles = default_user.roles
this.permissions = default_user.permissions
const groups = await default_user.groups()
for ( const group of groups ) {
group.user_ids.push(this.id)
await group.save()
}
}
async get_uid_number() {
if ( !this.uid_number ) {
const Setting = this.models.get('Setting')
let last_uid = await Setting.get('ldap.last_alloc_uid')
if ( last_uid < 1 ) {
last_uid = this.configs.get('ldap:server.schema.start_uid')
}
this.uid_number = last_uid + 1
await Setting.set('ldap.last_alloc_uid', this.uid_number)
await this.save()
}
return this.uid_number
}
async photo() { async photo() {
const File = this.models.get('upload::File') const File = this.models.get('upload::File')
return File.findById(this.photo_file_id) return File.findById(this.photo_file_id)
@ -77,10 +111,12 @@ class User extends AuthUser {
uid: this.uid, uid: this.uid,
first_name: this.first_name, first_name: this.first_name,
last_name: this.last_name, last_name: this.last_name,
name: `${this.first_name} ${this.last_name}`,
email: this.email, email: this.email,
tagline: this.tagline, tagline: this.tagline,
trap: this.trap, trap: this.trap,
group_ids: (await this.groups()).map(x => x.id), group_ids: (await this.groups()).map(x => x.id),
profile_photo: `${this.configs.get('app.url')}api/v1/auth/users/${this.uid}/photo`,
} }
} }
@ -119,13 +155,49 @@ class User extends AuthUser {
await this.save() await this.save()
} }
async check_credential_string(credential) {
// Check if the credentials are an app_password
const app_password_verified = Array.isArray(this.app_passwords)
&& this.app_passwords.length > 0
&& await this.check_app_password(credential)
// Check if the user has MFA enabled.
// If so, split the incoming password to fetch the MFA code
// e.g. normalPassword:123456
if ( !app_password_verified && this.mfa_enabled ) {
const parts = credential.split(':')
const mfa_code = parts.pop()
const actual_password = parts.join(':')
// Check the credentials
if ( !await this.check_password(actual_password) ) {
return false
}
// Now, check the MFA code
if ( !this.mfa_token.verify(mfa_code) ) {
return false
}
// If not MFA, just check the credentials
} else if (!app_password_verified && !await this.check_password(credential)) {
return false
}
return true
}
async check_password(password) { async check_password(password) {
return this.get_provider().check_user_auth(this, password) return this.get_provider().check_user_auth(this, password)
} }
async check_app_password(password) { async check_app_password(password) {
for ( const pw of this.app_passwords ) { for ( const pw of this.app_passwords ) {
if ( await pw.verify(password) ) return true if ( await pw.verify(password) ) {
pw.accessed = new Date
await pw.save()
return true
}
} }
return false return false
@ -169,20 +241,62 @@ class User extends AuthUser {
this.get_provider().logout(request) this.get_provider().logout(request)
} }
async has_sudo() {
const groups = await this.groups()
return groups.some(group => group.grants_sudo)
}
async to_sudo(iam_targets = []) {
const Policy = this.models.get('iam:Policy')
const granted = []
for ( const target of iam_targets ) {
if ( await Policy.check_user_access(this, target, 'sudo') ) {
granted.push(target)
}
}
return {
objectClass: ['sudoRole'],
cn: `sudo_${this.uid.toLowerCase()}`,
sudoUser: this.uid.toLowerCase(),
...(granted.length ? {
iamtarget: granted,
sudoHost: 'ALL',
sudoRunAs: 'ALL',
sudoCommand: 'ALL',
} : {})
}
}
async to_ldap(iam_targets = []) { async to_ldap(iam_targets = []) {
const Policy = this.models.get('iam:Policy') const Policy = this.models.get('iam:Policy')
const uid_number = await this.get_uid_number()
const shell = this.login_shell || this.configs.get('ldap:server.schema.default_shell')
const domain = this.configs.get('ldap:server.schema.base_dc').split(',').map(x => x.replace('dc=', '')).join('.')
const group_ids = []
for ( const group of await this.groups() ) {
group_ids.push(await group.get_gid_number())
}
const ldap_data = { const ldap_data = {
uid: this.uid, uid: this.uid.toLowerCase(),
uuid: this.uuid, uuid: this.uuid,
cn: this.first_name, cn: this.first_name,
sn: this.last_name, sn: this.last_name,
gecos: `${this.first_name} ${this.last_name}`, gecos: `${this.first_name} ${this.last_name}`,
mail: this.email, mail: this.email,
objectClass: ['inetOrgPerson', 'person'], objectClass: ['inetOrgPerson', 'person', 'posixaccount'],
objectclass: ['inetOrgPerson', 'person', 'posixaccount'],
entryuuid: this.uuid, entryuuid: this.uuid,
entryUUID: this.uuid, entryUUID: this.uuid,
objectGuid: this.uuid, objectGuid: this.uuid,
objectguid: this.uuid,
uidNumber: uid_number,
gidNumber: String(await this.get_uid_number()), // group_ids.map(x => String(x)),
loginShell: shell,
homeDirectory: `/home/${this.uid}@${domain}`
} }
if ( this.tagline ) ldap_data.extras_tagline = this.tagline if ( this.tagline ) ldap_data.extras_tagline = this.tagline
@ -213,7 +327,11 @@ class User extends AuthUser {
} }
get dn() { get dn() {
return LDAP.parseDN(`uid=${this.uid},${this.ldap_server.auth_dn().format(this.configs.get('ldap:server.format'))}`) return LDAP.parseDN(`uid=${this.uid.toLowerCase()},${this.ldap_server.auth_dn().format(this.configs.get('ldap:server.format'))}`)
}
get sudo_dn() {
return LDAP.parseDN(`cn=sudo_${this.uid.toLowerCase()},${this.ldap_server.sudo_dn().format(this.configs.get('ldap:server.format'))}`)
} }
// The following are used by OpenID connect // The following are used by OpenID connect
@ -227,15 +345,15 @@ class User extends AuthUser {
given_name: this.first_name, given_name: this.first_name,
locale: 'en_US', // TODO locale: 'en_US', // TODO
name: `${this.first_name} ${this.last_name}`, name: `${this.first_name} ${this.last_name}`,
preferred_username: this.uid, preferred_username: this.uid.toLowerCase(),
username: this.uid, username: this.uid.toLowerCase(),
} }
} }
static async findByLogin(login) { static async findByLogin(login) {
return this.findOne({ return this.findOne({
active: true, active: true,
uid: login, uid: login.toLowerCase(),
}) })
} }

View File

@ -0,0 +1,23 @@
const { Model } = require('flitter-orm')
class PermissionModel extends Model {
static get schema() {
return {
active: { type: Boolean, default: true },
target_type: String,
permission: String
}
}
async to_api() {
return {
_id: this.id,
id: this.id,
active: this.active,
target_type: this.target_type,
permission: this.permission,
}
}
}
module.exports = exports = PermissionModel

View File

@ -12,39 +12,49 @@ class PolicyModel extends Model {
entity_type: String, // user | group entity_type: String, // user | group
entity_id: String, entity_id: String,
access_type: String, // allow | deny access_type: String, // allow | deny
target_type: { type: String, default: 'application' }, // application | api_scope target_type: { type: String, default: 'application' }, // application | api_scope | machine | machine_group
target_id: String, target_id: String,
active: { type: Boolean, default: true }, active: { type: Boolean, default: true },
for_permission: { type: Boolean, default: false },
permission: String,
} }
} }
static async check_allow(entity_id, target_id) { static async check_allow(entity_id, target_id, permission = undefined) {
const policies = await this.find({ const policies = await this.find({
entity_id, entity_id,
target_id, target_id,
access_type: 'allow', access_type: 'allow',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
return policies.length > 0 return policies.length > 0
} }
static async check_deny(entity_id, target_id) { static async check_deny(entity_id, target_id, permission = undefined) {
const policies = await this.find({ const policies = await this.find({
entity_id, entity_id,
target_id, target_id,
access_type: 'deny', access_type: 'deny',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
return policies.length === 0 return policies.length === 0
} }
static async check_entity_access(entity_id, target_id) { static async check_entity_access(entity_id, target_id, permission = undefined) {
return (await this.check_allow(entity_id, target_id)) && !(await this.check_deny(entity_id, target_id)) return (await this.check_allow(entity_id, target_id, permission)) && !(await this.check_deny(entity_id, target_id, permission))
} }
static async check_user_denied(user, target_id) { static async check_user_denied(user, target_id, permission = undefined) {
const groups = await user.groups() const groups = await user.groups()
const group_ids = groups.map(x => x.id) const group_ids = groups.map(x => x.id)
@ -53,6 +63,10 @@ class PolicyModel extends Model {
target_id, target_id,
access_type: 'deny', access_type: 'deny',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
const group_denials = await this.find({ const group_denials = await this.find({
@ -60,41 +74,92 @@ class PolicyModel extends Model {
target_id, target_id,
access_type: 'deny', access_type: 'deny',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
return user_denials.length > 0 || group_denials.length > 0 return user_denials.length > 0 || group_denials.length > 0
} }
static async check_user_access(user, target_id) { static async get_all_related(target_id) {
const all = [target_id]
const Machine = this.prototype.models.get('ldap:Machine')
const MachineGroup = this.prototype.models.get('ldap:MachineGroup')
const machine = await Machine.findById(target_id)
if ( machine?.active ) {
const groups = await MachineGroup.find({
active: true,
machine_ids: machine.id,
})
groups.map(x => all.push(x.id))
}
const group = await MachineGroup.findById(target_id)
if ( group?.active ) {
const machines = await Machine.find({
active: true,
_id: {
$in: group.machine_ids.map(x => Machine.to_object_id(x)),
}
})
machines.map(x => all.push(x.id))
}
return all
}
static async check_user_access(user, target_id, permission = undefined) {
const groups = await user.groups() const groups = await user.groups()
const group_ids = groups.map(x => x.id) const group_ids = groups.map(x => x.id)
const target_ids = await this.get_all_related(target_id)
const user_approvals = await this.find({ const user_approvals = await this.find({
entity_id: user.id, entity_id: user.id,
target_id, target_id: { $in: target_ids },
access_type: 'allow', access_type: 'allow',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
const user_denials = await this.find({ const user_denials = await this.find({
entity_id: user.id, entity_id: user.id,
target_id, target_id: { $in: target_ids },
access_type: 'deny', access_type: 'deny',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
const group_approvals = await this.find({ const group_approvals = await this.find({
entity_id: { $in: group_ids }, entity_id: { $in: group_ids },
target_id, target_id: { $in: target_ids },
access_type: 'allow', access_type: 'allow',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
const group_denials = await this.find({ const group_denials = await this.find({
entity_id: { $in: group_ids }, entity_id: { $in: group_ids },
target_id, target_id: { $in: target_ids },
access_type: 'deny', access_type: 'deny',
active: true, active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
}) })
// IF user has explicit denial, deny // IF user has explicit denial, deny
@ -118,7 +183,7 @@ class PolicyModel extends Model {
if ( this.entity_type === 'user' ) { if ( this.entity_type === 'user' ) {
const User = this.models.get('auth:User') const User = this.models.get('auth:User')
const user = await User.findById(this.entity_id) const user = await User.findById(this.entity_id)
entity_display = `User: ${user.last_name}, ${user.first_name} (${user.uid})` entity_display = `User: ${user.last_name}, ${user.first_name} (${user.uid.toLowerCase()})`
} else if ( this.entity_type === 'group' ) { } else if ( this.entity_type === 'group' ) {
const Group = this.models.get('auth:Group') const Group = this.models.get('auth:Group')
const group = await Group.findById(this.entity_id) const group = await Group.findById(this.entity_id)
@ -132,6 +197,18 @@ class PolicyModel extends Model {
target_display = `Application: ${app.name}` target_display = `Application: ${app.name}`
} else if ( this.target_type === 'api_scope' ) { } else if ( this.target_type === 'api_scope' ) {
target_display = `API Scope: ${this.target_id}` target_display = `API Scope: ${this.target_id}`
} else if ( this.target_type === 'machine' ) {
const Machine = this.models.get('ldap:Machine')
const machine = await Machine.findById(this.target_id)
target_display = `Computer: ${machine.name}`
if ( machine.host_name ) {
target_display += ` (${machine.host_name})`
}
} else if ( this.target_type === 'machine_group' ) {
const MachineGroup = this.models.get('ldap:MachineGroup')
const group = await MachineGroup.findById(this.target_id)
target_display = `Computer Group: ${group.name} (${group.machine_ids.length} computers)`
} }
return { return {
@ -143,6 +220,8 @@ class PolicyModel extends Model {
target_display, target_display,
target_type: this.target_type, target_type: this.target_type,
target_id: this.target_id, target_id: this.target_id,
for_permission: this.for_permission,
permission: this.permission,
} }
} }
} }

View File

@ -19,7 +19,7 @@ class ClientModel extends Model {
const user = new User({ const user = new User({
first_name: name, first_name: name,
last_name: '(LDAP Agent)', last_name: '(LDAP Agent)',
uid, uid: uid.toLowerCase(),
roles: ['ldap_client'], roles: ['ldap_client'],
}) })
@ -58,7 +58,7 @@ class ClientModel extends Model {
id: this.id, id: this.id,
name: this.name, name: this.name,
user_id: user.id, user_id: user.id,
uid: user.uid, uid: user.uid.toLowerCase(),
last_invocation: this.last_invocation, last_invocation: this.last_invocation,
permissions: [...user.permissions, ...role_permissions], permissions: [...user.permissions, ...role_permissions],
} }

View File

@ -0,0 +1,73 @@
const { Model } = require('flitter-orm')
const LDAP = require('ldapjs')
const bcrypt = require('bcrypt')
class MachineModel extends Model {
static get services() {
return [...super.services, 'models', 'ldap_server', 'configs']
}
static get schema() {
return {
name: String,
bind_password: String,
description: String,
host_name: String,
location: String,
active: { type: Boolean, default: true },
ldap_visible: { type: Boolean, default: true },
}
}
async to_api() {
return {
id: this.id,
name: this.name,
description: this.description,
host_name: this.host_name,
location: this.location,
ldap_visible: this.ldap_visible,
iam_filter: `(|(iamTarget=${this.id}))`,
}
}
async groups() {
const MachineGroup = this.models.get('ldap:MachineGroup')
return MachineGroup.find({
machine_ids: this.id,
active: true
})
}
async set_bind_password(password) {
this.bind_password = await bcrypt.hash(password, 10)
return this
}
async check_bind_password(password) {
return await bcrypt.compare(password, this.bind_password)
}
get dn() {
return LDAP.parseDN(`cn=${this.name},${this.ldap_server.machine_dn().format(this.configs.get('ldap:server.format'))}`)
}
async to_ldap() {
const data = {
cn: this.name,
dn: this.dn.format(this.configs.get('ldap:server.format')),
name: this.name,
id: this.id,
objectClass: ['computer'],
description: this.description,
dNSHostName: this.host_name,
location: this.location,
primaryGroupID: 515, // compat with AD
sAMAccountType: 805306369, // compat with AD
}
return data;
}
}
module.exports = exports = MachineModel

View File

@ -0,0 +1,47 @@
const { Model } = require('flitter-orm')
const uuid = require('uuid').v4
const LDAP = require('ldapjs')
class MachineGroupModel extends Model {
static get services() {
return [...super.services, 'models', 'ldap_server', 'configs']
}
static get schema() {
return {
name: String,
description: String,
UUID: { type: String, default: uuid },
active: { type: Boolean, default: true },
machine_ids: [String],
ldap_visible: { type: Boolean, default: true },
}
}
async to_api() {
return {
id: this.id,
name: this.name,
description: this.description || '',
UUID: this.UUID,
machine_ids: this.machine_ids,
ldap_visible: this.ldap_visible,
}
}
get dn() {
return LDAP.parseDN(`cn=${this.name},${this.ldap_server.machine_group_dn().format(this.configs.get('ldap:server.format'))}`)
}
async to_ldap() {
return {
cn: this.name,
dn: this.dn.format(this.configs.get('ldap:server.format')),
id: this.id,
uuid: this.UUID,
description: this.description,
}
}
}
module.exports = exports = MachineGroupModel

View File

@ -0,0 +1,32 @@
const { Model } = require('flitter-orm')
const {v4: uuid} = require("uuid");
class Client extends Model {
static get services() {
return [...super.services, 'models']
}
static get schema() {
return {
name: String,
secret: {type: String, default: uuid},
active: {type: Boolean, default: true},
}
}
async application() {
const Application = this.models.get('Application')
return Application.findOne({ active: true, radius_client_ids: this.id })
}
async to_api() {
return {
id: this.id,
name: this.name,
secret: this.secret,
active: this.active,
}
}
}
module.exports = exports = Client

View File

@ -17,7 +17,9 @@ class SAMLRequestMiddleware extends Middleware {
// Verify that the issuer is known // Verify that the issuer is known
const sp = await ServiceProvider.findOne({entity_id: data.issuer, active: true}) const sp = await ServiceProvider.findOne({entity_id: data.issuer, active: true})
if (!sp) if (!sp)
return res.error(401, 'Unable to continue. The SAML issuer is unknown.') return res.error(401, {
message: 'Unable to continue. The SAML issuer is unknown.'
})
req.saml_request = { req.saml_request = {
relay_state: req.query.RelayState || req.body.RelayState, relay_state: req.query.RelayState || req.body.RelayState,

View File

@ -17,7 +17,7 @@ class SessionParticipantStore extends Injectable {
async issue({ service_provider }) { async issue({ service_provider }) {
const sp = new this.SessionParticipant({ const sp = new this.SessionParticipant({
service_provider_id: service_provider.id, service_provider_id: service_provider.id,
name_id: this.request.user.uid, name_id: this.request.user.uid.toLowerCase(),
// session_index: this.get_index(), // session_index: this.get_index(),
slo_url: service_provider.slo_url, slo_url: service_provider.slo_url,
// TODO sp_cert, // TODO sp_cert,

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