Compare commits

..

81 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
128 changed files with 7006 additions and 3480 deletions

View File

@ -1,88 +1,68 @@
---
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:
- name: release
image: plugins/gitea-release
settings:
api_key:
from_secret: gitea_api_key
base_url: https://code.garrettmills.dev
checksum: md5
title: ${DRONE_TAG}
- name: container build
image: docker:latest
privileged: true
commands:
- "while ! docker stats --no-stream; do sleep 1; done"
- "docker build -t $DOCKER_REGISTRY/starship/coreid ."
- "docker push $DOCKER_REGISTRY/starship/coreid"
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:
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 checkout master
- git pull
- git checkout ${DRONE_TAG}
- 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:
- tag
- 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
# Logs
logs
@ -150,3 +152,4 @@ tmp.uploads/*
!tmp.uploads/.gitkeep
uploads/*
!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'),
'LDAPRoutingUnit': require('./app/unit/LDAPRoutingUnit'),
'OpenIDConnect' : require('./app/unit/OpenIDConnectUnit'),
'Radius' : require('./app/unit/RadiusUnit'),
/*
* 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 template() { return template }
loading = false
constructor() {
super()
verify_code = ''
verify_success = false
this.loading = false
error_message = ''
other_message = ''
t = {}
this.verify_code = ''
this.verify_success = false
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() {
this.t = await T(

View File

@ -28,12 +28,16 @@ export default class MFADisableComponent extends Component {
static get template() { return template }
static get props() { return [] }
app_name = ''
step = 0
loading = false
error_message = ''
other_message = ''
t = {}
constructor() {
super()
this.app_name = ''
this.step = 0
this.loading = false
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() {
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 props() { return ['app_name'] }
verify_success = false
loading = false
recovery_code = ''
error_message = ''
other_message = ''
t = {}
constructor() {
super()
this.verify_success = false
this.loading = false
this.recovery_code = ''
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() {
this.t = await T(

View File

@ -61,19 +61,23 @@ export default class MFASetupPage extends Component {
static get props() { return ['app_name'] }
static get template() { return template }
loading = false
step = 0
constructor() {
super()
qr_data = ''
otpauth_url = ''
secret = ''
verify_code = ''
this.loading = false
this.step = 0
verify_success = false
this.qr_data = ''
this.otpauth_url = ''
this.secret = ''
this.verify_code = ''
error_message = ''
other_message = ''
t = {}
this.verify_success = false
this.error_message = ''
this.other_message = ''
this.t = {}
}
async vue_on_create() {
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 template() { return template }
loading = false
constructor() {
super()
this.loading = false
}
async action_click(index) {
this.loading = true

View File

@ -78,23 +78,27 @@ export default class PasswordResetComponent extends Component {
static get template() { return template }
static get props() { return ['app_name'] }
step = 0
loading = false
has_mfa = false
constructor() {
super()
error_message = ''
other_message = ''
this.step = 0
this.loading = false
this.has_mfa = false
step_1_valid = false
step_1_calc_time = ''
step_1_problem = ''
this.error_message = ''
this.other_message = ''
step_2_valid = false
this.step_1_valid = false
this.step_1_calc_time = ''
this.step_1_problem = ''
password = ''
confirm_password = ''
t = {}
ready = false
this.step_2_valid = false
this.password = ''
this.confirm_password = ''
this.t = {}
this.ready = false
}
async vue_on_create() {
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 }
username = ''
password = ''
button_text = ''
step_two = false
btn_disabled = true
loading = false
error_message = ''
other_message = ''
allow_back = true
auth_user = false
constructor() {
super()
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) {
this.btn_disabled = !new_username

View File

@ -98,19 +98,23 @@ export default class RegistrationFormComponent extends Component {
static get template() { return template }
static get props() { return ['app_name'] }
loading = false
step = 1
other_message = ''
error_message = ''
message = ''
btn_disabled = true
button_text = ''
constructor() {
super()
first_name = ''
last_name = ''
username = ''
email = ''
t = {}
this.loading = false
this.step = 1
this.other_message = ''
this.error_message = ''
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() {
// 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-html="typeof field.display === 'function' ? field.display(data) : field.display"
></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>
<select
:id="uuid+field.field"
@ -42,13 +42,13 @@ const template = `
<option
v-for="option of field.options"
: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>
</select>
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
</span>
<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
type="text"
class="form-control"
@ -146,20 +146,24 @@ export default class FormComponent extends Component {
return ['resource', 'form_id', 'initial_mode']
}
definition = {}
data = {}
uuid = ''
title = ''
error_message = ''
other_message = ''
constructor() {
super()
access_msg = ''
can_access = false
this.definition = {}
this.data = {}
this.uuid = ''
this.title = ''
this.error_message = ''
this.other_message = ''
is_ready = false
mode = ''
id = ''
t = {}
this.access_msg = ''
this.can_access = false
this.is_ready = false
this.mode = ''
this.id = ''
this.t = {}
}
reset() {
this.definition = {}

View File

@ -65,13 +65,17 @@ export default class ListingComponent extends Component {
static get template() { return template }
static get props() { return ['resource'] }
definition = {}
data = []
resource_class = {}
constructor() {
super()
access_msg = ''
can_access = false
t = {}
this.definition = {}
this.data = []
this.resource_class = {}
this.access_msg = ''
this.can_access = false
this.t = {}
}
async vue_on_create() {
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 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'
@ -22,6 +24,8 @@ const dash_components = {
ListingComponent,
FormComponent,
RootPageComponent,
OutletComponent,
}
export { dash_components }

View File

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

View File

@ -1,7 +1,7 @@
import { Component } from '../../lib/vues6/vues6.js'
import { event_bus } from '../service/EventBus.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 = `
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
@ -36,9 +36,9 @@ const template = `
aria-labelledby="navbarDropdown"
>
<h6 class="dropdown-header">Hello, {{ first_name }}.</h6>
<a href="/dash/profile" class="dropdown-item">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/system/Announcement" v-if="can.messages" class="dropdown-item">System Announcements</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" @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" @click="cobalt('system/Announcement', 'list')" class="dropdown-item" onclick="return false;">System Announcements</a>
<div class="dropdown-divider"></div>
<a href="/auth/logout" class="dropdown-item">Sign-Out of {{ app_name }}</a>
</div>
@ -53,10 +53,10 @@ export default class NavBarComponent extends Component {
static get template() { return template }
static get props() { return [] }
can = {}
constructor() {
super()
this.can = {}
this.toggle_event = event_bus.event('sidebar.toggle')
this.first_name = session.get('user.first_name')
this.last_name = session.get('user.last_name')
@ -72,4 +72,20 @@ export default class NavBarComponent extends Component {
toggle_sidebar() {
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,13 +23,18 @@ export default class SideBarComponent extends Component {
static get props() { return ['app_name'] }
static get template() { return template }
actions = []
constructor() {
super()
possible_actions = [
this.actions = []
this.isCollapsed = false
this.possible_actions = [
{
text: 'Profile',
action: 'redirect',
next: '/dash/profile',
action: 'navigate',
page: 'dash.profile',
},
{
text: 'Users',
@ -55,6 +60,24 @@ export default class SideBarComponent extends Component {
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',
@ -67,6 +90,12 @@ export default class SideBarComponent extends Component {
type: 'resource',
resource: 'oauth/Client',
},
{
text: 'RADIUS Clients',
action: 'list',
type: 'resource',
resource: 'radius/Client',
},
{
text: 'OpenID Connect Clients',
action: 'list',
@ -87,8 +116,6 @@ export default class SideBarComponent extends Component {
},
]
constructor() {
super()
event_bus.event('sidebar.toggle').subscribe(() => {
this.toggle()
})
@ -120,8 +147,6 @@ export default class SideBarComponent extends Component {
this.actions = new_actions
}
isCollapsed = false
toggle() {
this.isCollapsed = !this.isCollapsed
}

View File

@ -68,8 +68,12 @@ export default class MessageContainerComponent extends Component {
static get template() { return template }
static get props() { return [] }
messages = []
modals = []
constructor() {
super()
this.messages = []
this.modals = []
}
vue_on_create() {
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'
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-body">
<div class="row">
@ -77,6 +77,20 @@ const template = `
>
</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 class="list-group-item text-right font-italic text-muted">
{{ form_message }}
@ -90,6 +104,11 @@ const template = `
@click="change_password"
>{{ t['password.change'] }}</button>
</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')">
<h4>{{ t['mfa.mfa'] }}</h4>
<p>{{ t['profile.mfa_1'].replace(/APP_NAME/g, app_name) }}</p>
@ -117,6 +136,7 @@ const template = `
<div class="col-9">
{{ pw.name }}
<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 class="col-3 my-auto">
<button
@ -195,31 +215,36 @@ export default class EditProfileComponent extends Component {
static get template() { return template }
static get props() { return ['user_id'] }
profile_first = ''
profile_last = ''
profile_email = ''
profile_tagline = ''
last_reset = ''
mfa_enable_date = ''
constructor() {
super()
has_mfa_recovery = false
mfa_recovery_date = ''
mfa_recovery_codes = 0
this.profile_first = ''
this.profile_last = ''
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
ready = false
this.form_message = 'No changes.'
notify_gateway_url = ''
notify_app_key = ''
notify_enabled = false
notify_created_on = ''
notify_loaded = false
this.has_mfa = false
this.ready = false
app_passwords = []
app_name = ''
t = {}
this.notify_gateway_url = ''
this.notify_app_key = ''
this.notify_enabled = false
this.notify_created_on = ''
this.notify_loaded = false
this.app_passwords = []
this.app_name = ''
this.t = {}
}
on_key_up = ($event) => {}
@ -267,7 +292,14 @@ export default class EditProfileComponent extends Component {
'profile.app_key',
'profile.example_gateway_url',
'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')
@ -288,6 +320,7 @@ export default class EditProfileComponent extends Component {
last_name: this.profile_last,
email: this.profile_email,
tagline: this.profile_tagline,
login_shell: this.profile_shell,
user_id: this.user_id || 'me',
}
}
@ -336,6 +369,7 @@ export default class EditProfileComponent extends Component {
this.profile_last = result.last_name
this.profile_email = result.email
this.profile_tagline = result.tagline
this.profile_shell = result.login_shell
const notify_config = await profile_service.get_notify(this.user_id || 'me')
if ( !notify_config || !notify_config.has_config ) {
@ -380,6 +414,7 @@ export default class EditProfileComponent extends Component {
this.app_passwords = app_pws.map(x => {
if ( x.expires ) x.expires = (new Date(x.expires)).toLocaleDateString()
if ( x.created ) x.created = (new Date(x.created)).toLocaleDateString()
if ( x.accessed ) x.accessed = (new Date(x.accessed)).toLocaleDateString()
return x
})
}

View File

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

View File

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

View File

@ -2,14 +2,17 @@ import CRUDBase from './CRUDBase.js'
import { session } from '../service/Session.service.js'
class AppResource extends CRUDBase {
endpoint = '/api/v1/applications'
required_fields = ['name', 'identifier']
permission_base = 'v1:applications'
constructor() {
super()
item = 'Application'
plural = 'Applications'
this.endpoint = '/api/v1/applications'
this.required_fields = ['name', 'identifier']
this.permission_base = 'v1:applications'
listing_definition = {
this.item = 'Application'
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.
`,
@ -37,10 +40,10 @@ class AppResource extends CRUDBase {
},
{
position: 'main',
action: 'redirect',
action: 'navigate',
text: 'Setup Wizard',
color: 'success',
next: '/dash/app/setup',
page: 'app.setup',
},
{
type: 'resource',
@ -60,7 +63,7 @@ class AppResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
fields: [
{
name: 'Name',
@ -81,6 +84,14 @@ class AppResource extends CRUDBase {
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 application. (filter: iamTarget)`,
},
{
name: 'Associated LDAP Clients',
field: 'ldap_client_ids',
@ -101,6 +112,16 @@ class AppResource extends CRUDBase {
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',
@ -123,6 +144,7 @@ class AppResource extends CRUDBase {
},
],
}
}
}
const app = new AppResource()

View File

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

View File

@ -2,14 +2,17 @@ import CRUDBase from './CRUDBase.js'
import { session } from '../service/Session.service.js'
class SettingResource extends CRUDBase {
endpoint = '/api/v1/settings'
required_fields = ['key', 'value']
permission_base = 'v1:settings'
constructor() {
super()
item = 'Setting'
plural = 'Settings'
this.endpoint = '/api/v1/settings'
this.required_fields = ['key', 'value']
this.permission_base = 'v1:settings'
listing_definition = {
this.item = 'Setting'
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>
`,
@ -35,7 +38,7 @@ class SettingResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
fields: [
{
name: 'Setting Key',
@ -50,6 +53,7 @@ class SettingResource extends CRUDBase {
},
],
}
}
}
const setting = new SettingResource()

View File

@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js'
class GroupResource extends CRUDBase {
endpoint = '/api/v1/auth/groups'
required_fields = ['name']
permission_base = 'v1:auth:groups'
constructor() {
super()
item = 'Group'
plural = 'Groups'
this.endpoint = '/api/v1/auth/groups'
this.required_fields = ['name']
this.permission_base = 'v1:auth:groups'
listing_definition = {
this.item = 'Group'
this.plural = 'Groups'
this.listing_definition = {
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.
`,
@ -50,7 +53,7 @@ class GroupResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
fields: [
{
name: 'Name',
@ -59,6 +62,15 @@ class GroupResource extends CRUDBase {
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',
@ -71,6 +83,7 @@ class GroupResource extends CRUDBase {
},
],
}
}
}
const auth_group = new GroupResource()

View File

@ -1,12 +1,17 @@
import CRUDBase from '../CRUDBase.js'
class RoleResource extends CRUDBase {
endpoint = '/api/v1/auth/roles'
required_fields = ['role', 'permissions']
permission_base = 'v1:auth:roles'
item = 'Role'
plural = 'Roles'
constructor() {
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()

View File

@ -1,12 +1,16 @@
import CRUDBase from '../CRUDBase.js'
class TrapResource extends CRUDBase {
endpoint = '/api/v1/auth/traps'
required_fields = ['name', 'trap', 'redirect_to']
permission_base = 'v1:auth:traps'
constructor() {
super()
item = 'Trap'
plural = 'Traps'
this.endpoint = '/api/v1/auth/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()

View File

@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js'
class UserResource extends CRUDBase {
endpoint = '/api/v1/auth/users'
required_fields = ['uid', 'first_name', 'last_name', 'email']
permission_base = 'v1:auth:users'
constructor() {
super()
item = 'User'
plural = 'Users'
this.endpoint = '/api/v1/auth/users'
this.required_fields = ['uid', 'first_name', 'last_name', 'email']
this.permission_base = 'v1:auth:users'
listing_definition = {
this.item = 'User'
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.
`,
@ -57,7 +60,7 @@ class UserResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
fields: [
{
name: 'First Name',
@ -111,6 +114,7 @@ class UserResource extends CRUDBase {
},
],
}
}
}
const auth_user = new UserResource()

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,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js'
class PolicyResource extends CRUDBase {
endpoint = '/api/v1/iam/policy'
required_fields = ['entity_id', 'entity_type', 'target_id', 'target_type', 'access_type']
permission_base = 'v1:iam:policy'
constructor() {
super()
item = 'IAM Policy'
plural = 'IAM Policies'
this.endpoint = '/api/v1/iam/policy'
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'
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.
<br><br>
@ -38,6 +41,11 @@ class PolicyResource extends CRUDBase {
name: 'Target',
field: 'target_display',
},
{
name: 'Permission',
field: 'permission',
renderer: permission => permission || '-',
},
],
actions: [
{
@ -65,7 +73,7 @@ class PolicyResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
fields: [
{
name: 'Subject Type',
@ -73,8 +81,8 @@ class PolicyResource extends CRUDBase {
required: true,
type: 'select',
options: [
{ display: 'User', value: 'user' },
{ display: 'Group', value: 'group' },
{display: 'User', value: 'user'},
{display: 'Group', value: 'group'},
],
},
{
@ -107,8 +115,8 @@ class PolicyResource extends CRUDBase {
required: true,
type: 'select',
options: [
{ display: '...is granted access to...', value: 'allow' },
{ display: '...is denied access to...', value: 'deny' },
{display: '...is granted access to...', value: 'allow'},
{display: '...is denied access to...', value: 'deny'},
],
},
{
@ -117,8 +125,10 @@ class PolicyResource extends CRUDBase {
required: true,
type: 'select',
options: [
{ display: 'Application', value: 'application' },
{ display: 'API Scope', value: 'api_scope' },
{display: 'Application', value: 'application'},
{display: 'API Scope', value: 'api_scope'},
{display: 'Computer', value: 'machine'},
{display: 'Computer Group', value: 'machine_group'},
],
},
{
@ -145,6 +155,94 @@ class PolicyResource extends CRUDBase {
},
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: {
@ -155,6 +253,7 @@ class PolicyResource extends CRUDBase {
},
},*/
}
}
}
const iam_policy = new PolicyResource()

View File

@ -2,19 +2,18 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js'
class ClientResource extends CRUDBase {
endpoint = '/api/v1/ldap/clients'
required_fields = ['name', 'uid', 'password']
permission_base = 'v1:ldap:clients'
constructor() {
super()
item = 'LDAP Client'
plural = 'LDAP Clients'
this.endpoint = '/api/v1/ldap/clients'
this.required_fields = ['name', 'uid', 'password']
this.permission_base = 'v1:ldap:clients'
async server_config() {
const results = await axios.get('/api/v1/ldap/config')
if ( results && results.data && results.data.data ) return results.data.data
}
this.item = 'LDAP Client'
this.plural = 'LDAP Clients'
listing_definition = {
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.
<br><br>
@ -56,7 +55,7 @@ class ClientResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
fields: [
{
name: 'Provider Name',
@ -80,6 +79,12 @@ class ClientResource extends CRUDBase {
},
],
}
}
async server_config() {
const results = await axios.get('/api/v1/ldap/config')
if (results && results.data && results.data.data) return results.data.data
}
}
const ldap_client = new ClientResource()

View File

@ -1,14 +1,17 @@
import CRUDBase from '../CRUDBase.js'
class GroupResource extends CRUDBase {
endpoint = '/api/v1/ldap/groups'
required_fields = ['name', 'role']
permission_base = 'v1:ldap:groups'
constructor() {
super()
item = 'LDAP Group'
plural = 'LDAP Groups'
this.endpoint = '/api/v1/ldap/groups'
this.required_fields = ['name', 'role']
this.permission_base = 'v1:ldap:groups'
listing_definition = {
this.item = 'LDAP Group'
this.plural = 'LDAP Groups'
this.listing_definition = {
columns: [
{
name: 'Group Name',
@ -50,7 +53,7 @@ class GroupResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
// back_action: {
// text: 'Back',
// action: 'back',
@ -93,6 +96,7 @@ class GroupResource extends CRUDBase {
},
],
}
}
}
const ldap_group = new GroupResource()

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,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js';
class ClientResource extends CRUDBase {
endpoint = '/api/v1/oauth/clients'
required_fields = ['name', 'redirect_url', 'api_scopes']
permission_base = 'v1:oauth:clients'
constructor() {
super()
item = 'OAuth2 Client'
plural = 'OAuth2 Clients'
this.endpoint = '/api/v1/oauth/clients'
this.required_fields = ['name', 'redirect_url', 'api_scopes']
this.permission_base = 'v1:oauth:clients'
listing_definition = {
this.item = 'OAuth2 Client'
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.
<br><br>
@ -58,7 +61,7 @@ class ClientResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
fields: [
{
name: 'Client Name',
@ -101,6 +104,7 @@ class ClientResource extends CRUDBase {
},
],
}
}
}
const oauth_client = new ClientResource()

View File

@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js'
class ClientResource extends CRUDBase {
endpoint = '/openid/clients'
required_fields = ['client_name', 'grant_types', 'redirect_uri']
permission_base = 'v1:openid:clients'
constructor() {
super()
item = 'OpenID Connect Client'
plural = 'OpenID Connect Clients'
this.endpoint = '/openid/clients'
this.required_fields = ['client_name', 'grant_types', 'redirect_uri']
this.permission_base = 'v1:openid:clients'
listing_definition = {
this.item = 'OpenID Connect Client'
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.
`,
@ -49,7 +52,7 @@ class ClientResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
fields: [
{
name: 'Client Name',
@ -70,8 +73,8 @@ class ClientResource extends CRUDBase {
field: 'grant_types',
type: 'select.multiple',
options: [
{ display: 'Refresh Token', value: 'refresh_token' },
{ display: 'Authorization Code', value: 'authorization_code' },
{display: 'Refresh Token', value: 'refresh_token'},
{display: 'Authorization Code', value: 'authorization_code'},
],
required: true,
},
@ -91,6 +94,7 @@ class ClientResource extends CRUDBase {
},
],
}
}
}
const openid_client = new ClientResource()

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'
class ScopeResource extends CRUDBase {
endpoint = '/api/v1/reflect/scopes'
required_fields = ['scope']
permission_base = 'v1:reflect:scopes'
constructor() {
super()
item = 'API Scope'
plural = 'API Scopes'
this.endpoint = '/api/v1/reflect/scopes'
this.required_fields = ['scope']
this.permission_base = 'v1:reflect:scopes'
this.item = 'API Scope'
this.plural = 'API Scopes'
}
}
const reflect_scope = new ScopeResource()

View File

@ -1,14 +1,16 @@
import CRUDBase from '../CRUDBase.js'
class TokenResource extends CRUDBase {
endpoint = '/api/v1/reflect/tokens'
required_fields = ['client_id']
permission_base = 'v1:reflect:tokens'
constructor() {
super()
this.endpoint = '/api/v1/reflect/tokens'
this.required_fields = ['client_id']
this.permission_base = 'v1:reflect:tokens'
item = 'API Token'
plural = 'API Tokens'
this.item = 'API Token'
this.plural = 'API Tokens'
listing_definition = {
this.listing_definition = {
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.
<br><br>
@ -54,7 +56,7 @@ class TokenResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
fields: [
{
name: 'Client',
@ -83,6 +85,7 @@ class TokenResource extends CRUDBase {
},
],
}
}
}
const reflect_token = new TokenResource()

View File

@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
import { session } from '../../service/Session.service.js'
class ProviderResource extends CRUDBase {
endpoint = '/api/v1/saml/providers'
required_fields = ['name', 'acs_url', 'entity_id']
permission_base = 'v1:saml:providers'
constructor() {
super()
item = 'SAML Service Provider'
plural = 'SAML Service Providers'
this.endpoint = '/api/v1/saml/providers'
this.required_fields = ['name', 'acs_url', 'entity_id']
this.permission_base = 'v1:saml:providers'
listing_definition = {
this.item = 'SAML Service Provider'
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>
To do this, you need to know the SAML service provider's entity ID, assertion consumer service URL, and single-logout URL (if supported).`,
@ -58,7 +61,7 @@ class ProviderResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
fields: [
{
name: 'Provider Name',
@ -89,6 +92,7 @@ class ProviderResource extends CRUDBase {
},
],
}
}
}
const saml_provider = new ProviderResource()

View File

@ -1,14 +1,17 @@
import CRUDBase from '../CRUDBase.js'
class AnnouncementResource extends CRUDBase {
endpoint = '/api/v1/system/announcements'
required_fields = ['user_ids', 'group_ids', 'title', 'message', 'type']
permission_base = 'v1:system:announcements'
constructor() {
super()
item = 'System Announcement'
plural = 'System Announcements'
this.endpoint = '/api/v1/system/announcements'
this.required_fields = ['user_ids', 'group_ids', 'title', 'message', 'type']
this.permission_base = 'v1:system:announcements'
listing_definition = {
this.item = 'System Announcement'
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.
`,
@ -42,7 +45,7 @@ class AnnouncementResource extends CRUDBase {
],
}
form_definition = {
this.form_definition = {
fields: [
{
name: 'Title',
@ -79,9 +82,9 @@ class AnnouncementResource extends CRUDBase {
field: 'type',
type: 'select',
options: [
{ display: 'Login Intercept', value: 'login' },
{ display: 'E-Mail', value: 'email' },
{ display: 'System Banner', value: 'banner' },
{display: 'Login Intercept', value: 'login'},
{display: 'E-Mail', value: 'email'},
{display: 'System Banner', value: 'banner'},
],
},
],
@ -92,6 +95,7 @@ class AnnouncementResource extends CRUDBase {
},
}
}
}
}
const system_announcement = new AnnouncementResource()

View File

@ -1,5 +1,11 @@
import { location_service } from './Location.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 {
async perform({ text = '', action, ...args }) {
@ -7,21 +13,44 @@ class ActionService {
if ( args.next ) {
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' ) {
return location_service.back()
} else if ( args.type === 'resource' ) {
const { resource } = args
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' ) {
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' ) {
const { id } = args
const rsc = await resource_service.get(resource)
await rsc.delete(id)
} 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' ) {
const inputs = []

View File

@ -1,9 +1,9 @@
class Event {
firings = []
subscriptions = []
constructor(name) {
this.name = name
this.firings = []
this.subscriptions = []
}
subscribe(handler) {
@ -22,7 +22,9 @@ class Event {
}
class EventBusService {
_events = {}
constructor() {
this._events = {}
}
event(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'
class MessageService {
listener_interval = 25000
constructor() {
this.listener_interval = 25000
}
alert({type, message, timeout = 0, 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 {
async get_profile(user_id = 'me') {
@ -10,8 +12,11 @@ class ProfileService {
if ( results && results.data && results.data.data ) return results.data.data
}
async update_profile({ user_id, first_name, last_name, email, tagline = undefined }) {
await axios.patch(`/api/v1/profile/${user_id}`, { first_name, last_name, email, tagline })
async update_profile({ user_id, first_name, last_name, email, login_shell = undefined, tagline = undefined }) {
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 }) {

View File

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

View File

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

View File

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

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

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

@ -105,7 +105,7 @@ class OpenIDController extends Controller {
const Client = this.models.get('openid:Client')
const client = await Client.findById(req.params.id)
if ( !client || !client.active )
if ( !client )
return res.status(404)
.message(req.T('api.client_not_found'))
.api()
@ -153,6 +153,12 @@ class OpenIDController extends Controller {
})
}
// 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', {
...this.Vue.data({
message: `<h3 class="font-weight-light">Authorize ${application.name}?</h3>
@ -170,6 +176,11 @@ class OpenIDController extends Controller {
{
text: req.T('common.grant'),
action: 'redirect',
next: `/openid/grant-and-save/${application.id}/${uid.toLowerCase()}`,
},
{
text: req.T('common.grant_once'),
action: 'redirect',
next: `/openid/interaction/${uid.toLowerCase()}/grant`,
},
],
@ -177,6 +188,19 @@ class OpenIDController extends Controller {
})
}
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 }) {
return res.redirect(`/openid/interaction/${uid.toLowerCase()}/start-session`)
}

View File

@ -115,6 +115,28 @@ class AppController extends Controller {
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
const OpenIDClient = this.models.get('openid:Client')
if ( req.body.openid_client_ids ) {
@ -242,6 +264,28 @@ class AppController extends Controller {
application.oauth_client_ids = 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
const OpenIDClient = this.models.get('openid:Client')
if ( req.body.openid_client_ids ) {

View File

@ -91,6 +91,7 @@ class AuthController extends Controller {
if ( !(await User.findOne()) ) user.promote('root')
await user.save()
await user.grant_defaults()
// Log in the user automatically
await this.auth.get_provider().session(req, user)
@ -219,6 +220,48 @@ class AuthController extends Controller {
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) {
if ( !req.user.can(`auth:group:create`) )
return res.status(401)
@ -239,7 +282,10 @@ class AuthController extends Controller {
.message(req.T('api.group_already_exists'))
.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
const User = this.models.get('auth:User')
@ -258,6 +304,7 @@ class AuthController extends Controller {
}
await group.save()
await group.get_gid_number()
return res.api(await group.to_api())
}
@ -317,6 +364,7 @@ class AuthController extends Controller {
await user.reset_password(req.body.password, 'create')
await user.save()
await user.grant_defaults()
return res.api(await user.to_api())
}
@ -365,7 +413,10 @@ class AuthController extends Controller {
}
group.name = req.body.name
group.grants_sudo = !!req.body.grants_sudo
await group.save()
await group.get_gid_number()
return res.api()
}

View File

@ -13,7 +13,7 @@ class IAMController extends Controller {
.message(`${req.T('api.missing_field', true)} entity_id, target_id`)
.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) {
@ -39,7 +39,7 @@ class IAMController extends Controller {
.message(req.T('api.insufficient_permissions'))
.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) {
@ -56,6 +56,33 @@ class IAMController extends Controller {
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) {
const Policy = this.models.get('iam:Policy')
const policy = await Policy.findById(req.params.id)
@ -73,6 +100,23 @@ class IAMController extends Controller {
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) {
const Policy = this.models.get('iam:Policy')
@ -108,12 +152,12 @@ class IAMController extends Controller {
if ( !['allow', 'deny'].includes(req.body.access_type) )
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()
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)
.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()
// Make sure the target_id is valid
@ -130,6 +174,20 @@ class IAMController extends Controller {
return res.status(400)
.message(`${req.T('common.invalid')} target_id.`)
.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({
@ -140,12 +198,71 @@ class IAMController extends Controller {
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()
req.user.allow(`iam:policy:${policy.id}`)
await req.user.save()
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) {
const Policy = this.models.get('iam:Policy')
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.`)
.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)
.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()
// Make sure the target_id is valid
@ -214,6 +331,20 @@ class IAMController extends Controller {
return res.status(400)
.message(`${req.T('common.invalid')} target_id.`)
.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
@ -221,10 +352,69 @@ class IAMController extends Controller {
policy.access_type = req.body.access_type
policy.target_type = req.body.target_type
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()
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) {
const Policy = this.models.get('iam:Policy')
const policy = await Policy.findById(req.params.id)
@ -243,6 +433,27 @@ class IAMController extends Controller {
await policy.save()
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

View File

@ -46,6 +46,32 @@ class LDAPController extends Controller {
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) {
const Client = this.models.get('ldap:Client')
const client = await Client.findById(req.params.id)
@ -80,6 +106,40 @@ class LDAPController extends Controller {
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) {
if ( !req.user.can('ldap:client:create') )
return res.status(401)
@ -121,13 +181,89 @@ class LDAPController extends Controller {
return res.api(await client.to_api())
}
async create_group(req, res, next) {
console.log(req.body)
if ( !req.user.can(`ldap:group:create`) )
return res.status(401)
.message(req.T('api.insufficient_permissions'))
async create_machine(req, res, next) {
// validate inputs
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 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()
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
const required_fields = ['role', 'name']
for ( const field of required_fields ) {
@ -240,6 +376,106 @@ class LDAPController extends Controller {
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) {
const User = await this.models.get('auth:User')
const Group = await this.models.get('ldap:Group')
@ -337,6 +573,44 @@ class LDAPController extends Controller {
await group.save()
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

View File

@ -20,6 +20,7 @@ class PasswordController extends Controller {
return {
created: x.created,
expires: x.expires,
accessed: x.accessed,
active: x.active,
name: x.name ?? req.T('common.unnamed'),
uuid: x.uuid,
@ -90,6 +91,10 @@ class PasswordController extends Controller {
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.session.registrant_flow ) {
await req.trap.begin('registrant_flow', { session_only: true })
}
// invalidate existing tokens and other logins
await req.user.logout(req)
await req.user.kickout()

View File

@ -24,8 +24,9 @@ class ProfileController extends Controller {
last_name: user.last_name,
email: user.email,
uid: user.uid,
tagline: user.tagline,
tagline: user.tagline || '',
user_id: user.id,
login_shell: user.login_shell || '',
...(user.notify_config ? { notify_config: await user.notify_config.to_api() } : {})
})
}
@ -123,6 +124,8 @@ class ProfileController extends Controller {
async update(req, res, next) {
const User = this.models.get('auth:User')
const Message = this.models.get('Message')
const Setting = this.models.get('Setting')
let user
if ( req.params.user_id === 'me' ) user = req.user
@ -154,14 +157,22 @@ class ProfileController extends Controller {
.api()
// 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.last_name = req.body.last_name
user.email = req.body.email
user.tagline = req.body.tagline
user.login_shell = req.body.login_shell
// Save the record
await user.save()
return res.api()
return res.api({
force_message_refresh: true,
})
}
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 {
static get services() {
return [...super.services, 'Vue', 'models']
return [...super.services, 'Vue', 'models', 'jobs']
}
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', {
...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) {
const Setting = this.models.get('Setting')

View File

@ -23,13 +23,13 @@ class Oauth2 extends Oauth2Controller {
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!')
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!')
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',
@ -44,7 +44,7 @@ class Oauth2 extends Oauth2Controller {
async authorize_get(req, res, next) {
const client = await this._get_authorize_client(req)
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 starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID })
@ -54,19 +54,25 @@ class Oauth2 extends Oauth2Controller {
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!')
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!')
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) ) {
return this.Vue.invoke_action(res, {
text: 'Grant Access',

View File

@ -12,7 +12,7 @@ class EMailJob extends Job {
const { data } = job
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)
@ -20,9 +20,11 @@ class EMailJob extends Job {
from, to, subject, html,
})
} 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 = '' }) {

View File

@ -12,7 +12,7 @@ class ForeignIPLoginAlertJob extends Job {
const user = await User.findById(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', {
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 ) {
await user.notify_config.log({
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.`,
})
this.info('Logged push notification job')
}
} 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 = await User.findById(user_id)
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.')
}
this.output.info(`Resetting password for user: ${user.uid}`)
this.info(`Resetting password for user: ${user.uid}`)
// Create an authenticated key-action
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', {
to: user.email,
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 ) {
await user.notify_config.log({
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.`,
priority: 8,
})
this.info('Logged security push notification job')
}
this.output.success('Password reset logged.')
this.success('Password reset logged.')
} 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)
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', {
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 ) {
await user.notify_config.log({
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.`,
priority: 8,
})
this.info('Logged push notification job')
}
} 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)
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.')
}
await announcement.populate()
this.output.success('Populated announcements.')
this.success('Populated announcements.')
} 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
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 })
} 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 LDAP = require('ldapjs')
class GroupsController extends LDAPController {
static get services() {

View File

@ -59,34 +59,8 @@ class LDAPController extends Injectable {
return next(new LDAP.InsufficientAccessRightsError())
}
// Check if the credentials are an app_password
const app_password_verified = Array.isArray(item.app_passwords)
&& 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`)
// Check if the credentials are valid
if ( !(await item.check_credential_string(req.credentials)) ) {
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

@ -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],
oauth_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,
oauth_client_ids: this.oauth_client_ids,
openid_client_ids: this.openid_client_ids,
radius_client_ids: this.radius_client_ids || [],
}
}
}

View File

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

View File

@ -11,6 +11,9 @@ class GroupModel extends Model {
return {
name: String,
user_ids: [String],
posix_user_id: String,
posix_group_id: Number,
grants_sudo: { type: Boolean, default: false },
active: { 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)) } })
}
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() {
const users = await this.users()
return {
cn: this.name,
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'))),
}
}
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() {
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() {
@ -49,6 +106,7 @@ class GroupModel extends Model {
name: this.name,
user_ids: this.user_ids,
ldap_visible: this.ldap_visible,
grants_sudo: !!this.grants_sudo,
}
}
}

View File

@ -26,6 +26,7 @@ class User extends AuthUser {
last_name: String,
tagline: String,
email: String,
email_verified: {type: Boolean, default: false},
ldap_visible: {type: Boolean, default: true},
active: {type: Boolean, default: true},
mfa_token: MFAToken,
@ -38,9 +39,42 @@ class User extends AuthUser {
photo_file_id: String,
trap: String,
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() {
const File = this.models.get('upload::File')
return File.findById(this.photo_file_id)
@ -77,10 +111,12 @@ class User extends AuthUser {
uid: this.uid,
first_name: this.first_name,
last_name: this.last_name,
name: `${this.first_name} ${this.last_name}`,
email: this.email,
tagline: this.tagline,
trap: this.trap,
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()
}
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) {
return this.get_provider().check_user_auth(this, password)
}
async check_app_password(password) {
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
@ -169,9 +241,45 @@ class User extends AuthUser {
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 = []) {
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 = {
uid: this.uid.toLowerCase(),
uuid: this.uuid,
@ -179,10 +287,16 @@ class User extends AuthUser {
sn: this.last_name,
gecos: `${this.first_name} ${this.last_name}`,
mail: this.email,
objectClass: ['inetOrgPerson', 'person'],
objectClass: ['inetOrgPerson', 'person', 'posixaccount'],
objectclass: ['inetOrgPerson', 'person', 'posixaccount'],
entryuuid: this.uuid,
entryUUID: 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
@ -216,6 +330,10 @@ class User extends AuthUser {
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
async claims(use, scope) {

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_id: String,
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,
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({
entity_id,
target_id,
access_type: 'allow',
active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
})
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({
entity_id,
target_id,
access_type: 'deny',
active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
})
return policies.length === 0
}
static async check_entity_access(entity_id, target_id) {
return (await this.check_allow(entity_id, target_id)) && !(await this.check_deny(entity_id, target_id))
static async check_entity_access(entity_id, target_id, permission = undefined) {
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 group_ids = groups.map(x => x.id)
@ -53,6 +63,10 @@ class PolicyModel extends Model {
target_id,
access_type: 'deny',
active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
})
const group_denials = await this.find({
@ -60,41 +74,92 @@ class PolicyModel extends Model {
target_id,
access_type: 'deny',
active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
})
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 group_ids = groups.map(x => x.id)
const target_ids = await this.get_all_related(target_id)
const user_approvals = await this.find({
entity_id: user.id,
target_id,
target_id: { $in: target_ids },
access_type: 'allow',
active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
})
const user_denials = await this.find({
entity_id: user.id,
target_id,
target_id: { $in: target_ids },
access_type: 'deny',
active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
})
const group_approvals = await this.find({
entity_id: { $in: group_ids },
target_id,
target_id: { $in: target_ids },
access_type: 'allow',
active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
})
const group_denials = await this.find({
entity_id: { $in: group_ids },
target_id,
target_id: { $in: target_ids },
access_type: 'deny',
active: true,
...(permission ? {
for_permission: true,
permission,
} : {})
})
// IF user has explicit denial, deny
@ -132,6 +197,18 @@ class PolicyModel extends Model {
target_display = `Application: ${app.name}`
} else if ( this.target_type === 'api_scope' ) {
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 {
@ -143,6 +220,8 @@ class PolicyModel extends Model {
target_display,
target_type: this.target_type,
target_id: this.target_id,
for_permission: this.for_permission,
permission: this.permission,
}
}
}

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
const sp = await ServiceProvider.findOne({entity_id: data.issuer, active: true})
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 = {
relay_state: req.query.RelayState || req.body.RelayState,

View File

@ -58,7 +58,29 @@ class TrapUtility {
allows(route) {
const config = this.config()
return route.startsWith('/assets') || config.allowed_routes.includes(route.toLowerCase().trim())
const allowed = route.startsWith('/assets') || config.allowed_routes.includes(route.toLowerCase().trim())
if ( allowed ) return true
for ( const allowed_route of config.allowed_routes ) {
console.log('comparing', allowed_route, 'to', route)
const allowed_parts = allowed_route.split('/')
const parts = route.split('/')
let matches = true
for ( let i = 0; i < allowed_parts.length; i += 1 ) {
if ( allowed_parts[i] !== parts[i] && allowed_parts[i] !== '*' ) {
matches = false
}
}
if ( matches ) {
console.log('allows true')
return true
}
}
console.log('allows false')
return false
}
}
@ -68,8 +90,19 @@ class TrapsMiddleware extends Middleware {
}
async test(req, res, next, args = {}) {
const Setting = this.models.get('Setting')
req.trap = new TrapUtility(req, res, this.configs.get('traps.types'))
if (
!req.trap.has_trap()
&& req.user
&& !req.user.email_verified
&& (await Setting.get('auth.require_email_verify'))
) {
req.session.email_verify_flow = req.originalUrl
await req.trap.begin('verify_email', { session_only: false })
}
if ( !req.trap.has_trap() ) return next()
else if ( req.trap.allows(req.path) ) return next()
else return req.trap.redirect()

View File

@ -36,6 +36,14 @@ const auth_routes = {
['middleware::api:Permission', { check: 'v1:auth:users:get' }],
'controller::api:v1:Auth.get_user',
],
'/users/:id/flat': [
'middleware::auth:APIRoute',
['middleware::api:Permission', { check: 'v1:auth:users:get' }],
'controller::api:v1:Auth.get_user_flat',
],
'/users/:id/photo': [
'controller::api:v1:Auth.get_user_photo',
],
'/groups/:id': [
'middleware::auth:APIRoute',
['middleware::api:Permission', { check: 'v1:auth:groups:get' }],

View File

@ -14,6 +14,14 @@ const iam_routes = {
['middleware::api:Permission', { check: 'v1:iam:policy:get' }],
'controller::api:v1:IAM.get_policy',
],
'/permission': [
['middleware::api:Permission', { check: 'v1:iam:permission:list' }],
'controller::api:v1:IAM.get_permissions',
],
'/permission/:id': [
['middleware::api:Permission', { check: 'v1:iam:permission:get' }],
'controller::api:v1:IAM.get_permission',
],
},
post: {
@ -21,6 +29,10 @@ const iam_routes = {
['middleware::api:Permission', { check: 'v1:iam:policy:create' }],
'controller::api:v1:IAM.create_policy',
],
'/permission': [
['middleware::api:Permission', { check: 'v1:iam:permission:create' }],
'controller::api:v1:IAM.create_permission',
],
'/check_entity_access': [
['middleware::api:Permission', { check: 'v1:iam:check_entity_access' }],
'controller::api:v1:IAM.check_entity_access',
@ -36,6 +48,10 @@ const iam_routes = {
['middleware::api:Permission', { check: 'v1:iam:policy:update' }],
'controller::api:v1:IAM.update_policy',
],
'/permission/:id': [
['middleware::api:Permission', { check: 'v1:iam:permission:update' }],
'controller::api:v1:IAM.update_permission',
],
},
delete: {
@ -43,6 +59,10 @@ const iam_routes = {
['middleware::api:Permission', { check: 'v1:iam:policy:delete' }],
'controller::api:v1:IAM.delete_policy',
],
'/permission/:id': [
['middleware::api:Permission', { check: 'v1:iam:permission:delete' }],
'controller::api:v1:IAM.delete_permission',
],
},
}

View File

@ -22,6 +22,22 @@ const ldap_routes = {
['middleware::api:Permission', { check: 'v1:ldap:groups:get' }],
'controller::api:v1:LDAP.get_group',
],
'/machines': [
['middleware::api:Permission', { check: 'v1:ldap:machines:list' }],
'controller::api:v1:LDAP.get_machines',
],
'/machines/:id': [
['middleware::api:Permission', { check: 'v1:ldap:machines:get' }],
'controller::api:v1:LDAP.get_machine',
],
'/machine-groups': [
['middleware::api:Permission', { check: 'v1:ldap:machine_groups:list' }],
'controller::api:v1:LDAP.get_machine_groups',
],
'/machine-groups/:id': [
['middleware::api:Permission', { check: 'v1:ldap:machine_groups:get' }],
'controller::api:v1:LDAP.get_machine_group',
],
'/config': [
['middleware::api:Permission', { check: 'v1:ldap:config:get' }],
'controller::api:v1:LDAP.get_config',
@ -37,6 +53,14 @@ const ldap_routes = {
['middleware::api:Permission', { check: 'v1:ldap:groups:create' }],
'controller::api:v1:LDAP.create_group',
],
'/machines': [
['middleware::api:Permission', { check: 'v1:ldap:machines:create' }],
'controller::api:v1:LDAP.create_machine',
],
'/machine-groups': [
['middleware::api:Permission', { check: 'v1:ldap:machine_groups:create' }],
'controller::api:v1:LDAP.create_machine_group',
],
},
patch: {
@ -48,6 +72,14 @@ const ldap_routes = {
['middleware::api:Permission', { check: 'v1:ldap:groups:update' }],
'controller::api:v1:LDAP.update_group',
],
'/machines/:id': [
['middleware::api:Permission', { check: 'v1:ldap:machines:update' }],
'controller::api:v1:LDAP.update_machine',
],
'/machine-groups/:id': [
['middleware::api:Permission', { check: 'v1:ldap:machine_groups:update' }],
'controller::api:v1:LDAP.update_machine_group',
],
},
delete: {
@ -59,6 +91,14 @@ const ldap_routes = {
['middleware::api:Permission', { check: 'v1:ldap:groups:delete' }],
'controller::api:v1:LDAP.delete_group',
],
'/machines/:id': [
['middleware::api:Permission', { check: 'v1:ldap:machines:delete' }],
'controller::api:v1:LDAP.delete_machine',
],
'/machine-groups/:id': [
['middleware::api:Permission', { check: 'v1:ldap:machine_groups:delete' }],
'controller::api:v1:LDAP.delete_machine_group',
],
},
}

View File

@ -0,0 +1,48 @@
const saml_routes = {
prefix: '/api/v1/radius',
middleware: [],
get: {
'/clients': [
['middleware::auth:APIRoute'],
['middleware::api:Permission', { check: 'v1:radius:clients:list' }],
'controller::api:v1:Radius.get_clients',
],
'/clients/:id': [
['middleware::auth:APIRoute'],
['middleware::api:Permission', { check: 'v1:radius:clients:get' }],
'controller::api:v1:Radius.get_client',
],
},
post: {
'/attempt': [
['middleware::auth:GuestOnly'],
'controller::api:v1:Radius.attempt',
],
'/clients': [
['middleware::auth:APIRoute'],
['middleware::api:Permission', { check: 'v1:radius:clients:create' }],
'controller::api:v1:Radius.create_client',
],
},
patch: {
'/clients/:id': [
['middleware::auth:APIRoute'],
['middleware::api:Permission', { check: 'v1:radius:clients:update' }],
'controller::api:v1:Radius.update_client',
],
},
delete: {
'/clients/:id': [
['middleware::auth:APIRoute'],
['middleware::api:Permission', { check: 'v1:radius:clients:delete' }],
'controller::api:v1:Radius.delete_client',
],
},
}
module.exports = exports = saml_routes

View File

@ -67,6 +67,21 @@ const index = {
'controller::auth:Forms.logout_provider_present_success',
],
'/finish-registration': [
'middleware::auth:UserOnly',
'controller::auth:Forms.finish_registration',
],
'/verify-email': [
'middleware::auth:UserOnly',
'controller::auth:Forms.show_verify_email',
],
'/verify-email/sent': [
'middleware::auth:UserOnly',
'controller::auth:Forms.send_verify_email',
],
'/login-message': [
'middleware::auth:UserOnly',
'controller::api:v1:System.show_login_message',

View File

@ -7,6 +7,9 @@ const openid = {
],
get: {
'/grant-and-save/:app_id/:uid': [
'middleware::auth:UserOnly', 'controller::OpenID.grant_and_save',
],
'/interaction/:uid': [
'controller::OpenID.handle_interaction',
],

View File

@ -2,6 +2,7 @@ const Unit = require('libflitter/Unit')
const LDAP = require('ldapjs')
const Validator = require('email-validator')
const net = require('net')
const fs = require('fs')
// TODO support logging ALL ldap requests when in DEBUG, not just routed ones
// TODO need to support LDAP server auto-discovery/detection features
@ -36,6 +37,18 @@ class LDAPServerUnit extends Unit {
return this.build_dn(this.config.schema.group_base)
}
machine_dn() {
return this.build_dn(this.config.schema.machine_base)
}
machine_group_dn() {
return this.build_dn(this.config.schema.machine_group_base)
}
sudo_dn() {
return this.build_dn(this.config.schema.sudo_base)
}
/**
* Get the anonymous DN.
* @returns {ldap/DN}
@ -77,7 +90,11 @@ class LDAPServerUnit extends Unit {
// If Flitter is configured to use an SSL certificate,
// use it to enable LDAPS in the server.
if ( this.express.use_ssl() ) {
if ( this.config.ssl?.enable ) {
this.output.info('Using configured SSL certificate. The LDAP server will require an ldaps:// connection.')
server_config.certificate = fs.readFileSync(this.config.ssl.certificate)
server_config.key = fs.readFileSync(this.config.ssl.key)
} else if ( this.express.use_ssl() ) {
this.output.info('Using configured SSL certificate. The LDAP server will require an ldaps:// connection.')
server_config.certificate = await this.express.ssl_certificate()
server_config.key = await this.express.ssl_key()

View File

@ -1,3 +1,4 @@
const fs = require('fs')
const Unit = require('libflitter/Unit')
const { Provider, interactionPolicy: { Prompt, base: policy } } = require('oidc-provider')
const uuid = require('uuid').v4
@ -14,6 +15,15 @@ class OpenIDConnectUnit extends Unit {
return [...super.services, 'output', 'configs', 'models']
}
load_jwks(file) {
if ( fs.existsSync(file) ) {
const content = fs.readFileSync(file)
try {
return JSON.parse(content)
} catch (e) {}
}
}
async go(app) {
this.Vue = this.app.di().get('Vue')
const issuer = this.configs.get('app.url')
@ -23,9 +33,13 @@ class OpenIDConnectUnit extends Unit {
CoreIDAdapter.connect(app)
const jwks_file = this.configs.get('oidc.jwks_file')
const jwks = this.load_jwks(jwks_file)
this.provider = new Provider(issuer, {
adapter: CoreIDAdapter,
clients: [],
jwks,
interactions: {
interactions,
url: (ctx, interaction) => `/openid/interaction/${ctx.oidc.uid.toLowerCase()}`,
@ -58,6 +72,15 @@ class OpenIDConnectUnit extends Unit {
...configuration,
})
const reportError = ({ headers: { authorization }, oidc: { body, client } }, err) => {
this.output.error('OpenIDConnect authorization error!')
this.output.error(err)
}
this.provider.on('grant.error', reportError)
this.provider.on('introspection.error', reportError)
this.provider.on('revocation.error', reportError)
if ( configuration.proxy ) this.provider.proxy = true
app.express.use('/oidc', this.wrap(this.provider.callback))
}
@ -91,6 +114,11 @@ class OpenIDConnectUnit extends Unit {
}
}
// Stupid /jwks only listens on GET which is incompatible w/ some apps
if ( req.url === '/jwks' ) {
req.method = 'GET'
}
return callback(req, res, next)
}
}

63
app/unit/RadiusUnit.js Normal file
View File

@ -0,0 +1,63 @@
const fs = require('fs/promises')
const uuid = require('uuid')
const { Unit } = require('libflitter')
const CoreIDAuthentication = require('../classes/radius/CoreIDAuthentication')
const net = require("net");
class RadiusUnit extends Unit {
static get services() {
return [...super.services, 'configs', 'output', 'models']
}
async go(app) {
if ( !this.configs.get('radius.enable') ) return;
const CoreIDRadiusServer = (await import('../classes/radius/CoreIDRadiusServer.mjs')).default
// Load the certificates
const pubkey = await fs.readFile(this.configs.get('radius.cert_file.public'))
const privkey = await fs.readFile(this.configs.get('radius.cert_file.private'))
this.radius = new CoreIDRadiusServer({
// logger
secret: this.configs.get('radius.secret', uuid.v4()),
port: this.configs.get('radius.port', 1812),
address: this.configs.get('radius.interface', '0.0.0.0'),
tlsOptions: {
cert: pubkey,
key: privkey,
},
authentication: new CoreIDAuthentication(),
})
if ( await this.port_free() ) {
this.output.info('Starting RADIUS server...')
await this.radius.start()
} else {
this.output.error('Will not start RADIUS server. Reason: configured port is already in use')
delete this.radius
}
}
async cleanup(app) {
if ( this.radius ) {
await this.radius.server.close()
}
}
async port_free() {
return new Promise((res, rej) => {
const server = net.createServer()
server.once('error', (e) => {
res(false)
})
server.once('listening', () => {
server.close()
res(true)
})
server.listen(this.configs.get('radius.port', 1812))
})
}
}
module.exports = exports = RadiusUnit

View File

@ -13,6 +13,22 @@ class SettingsUnit extends Unit {
Error.stackTraceLimit = 50
app.express.set('trust proxy', true)
const User = this.models.get('auth:User')
const user = await User.findOne({is_default_user_for_coreid: true})
if ( !user ) {
const user = new User({
uid: '__coreid_default_user__',
provider: 'flitter',
block_login: true,
first_name: 'Default_User',
last_name: 'Default_User',
ldap_visible: false,
is_default_user_for_coreid: true,
})
await user.save()
}
const Setting = this.models.get('Setting')
const default_settings = this.configs.get('setting.settings')
for ( const key in default_settings ) {
@ -21,6 +37,15 @@ class SettingsUnit extends Unit {
this.output.debug(`Guarantee setting key "${key}" with default value "${default_value}".`)
await Setting.guarantee(key, default_value)
}
const Permission = this.models.get('iam:Permission')
const default_permissions = this.configs.get('auth.iam.default_permissions')
for ( const perm of default_permissions ) {
const existing = await Permission.findOne(perm)
if ( !existing ) {
await (new Permission(perm)).save()
}
}
}
}

View File

@ -4,5 +4,4 @@ block content
.cobalt-container
.row.pad-top
.col-12
cobalt-form(v-if="form_id" :resource="resource" :form_id="form_id" :initial_mode="mode")
cobalt-form(v-if="!form_id" :resource="resource" :initial_mode="mode")
coreid-outlet(initial_page="cobalt.form" :initial_resource="resource" :initial_form_id="form_id" :initial_mode="mode")

View File

@ -4,4 +4,4 @@ block content
.cobalt-container
.row.pad-top
.col-12
cobalt-listing(:resource="resource")
coreid-outlet(initial_page="cobalt.listing" :initial_resource="resource")

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