Compare commits
82 Commits
Author | SHA1 | Date | |
---|---|---|---|
775ac8b474 | |||
3d6908b7ec | |||
0b8c4b87df | |||
1b12af0cd2 | |||
49be0887d0 | |||
d63de520c9 | |||
0d24782691 | |||
35113ed81c | |||
562ada3af5 | |||
04ea16743d | |||
cf91063315 | |||
fd8a05446a | |||
dae06aa577 | |||
ffbcf1b514 | |||
6e161dd383 | |||
64bc167d01 | |||
bd69be7137 | |||
f98f35f626 | |||
cf1ea362cd | |||
7f338325b9 | |||
a60af453ab | |||
670b9b1299 | |||
5f0d67d525 | |||
1852be4ef0 | |||
54258ecb8b | |||
1e80da9b80 | |||
5420cf58bd | |||
159fdb15e6 | |||
6612eb7b10 | |||
2fe1d499f6 | |||
cb783ea277 | |||
2f2d38d12f | |||
62c818dc8d | |||
f45e92af1e | |||
ced3a15d00 | |||
9729de47f8 | |||
5bc98e6568 | |||
de20dce735 | |||
13af63a364 | |||
3730ddc2f2 | |||
5391c7c6d6 | |||
ae85c3fd24 | |||
3301a48750 | |||
d1312fe627 | |||
bd6eaceaf3 | |||
6b2257ae33 | |||
636e1f8ab9 | |||
627499537d | |||
7e3f198c04 | |||
b26519ea88 | |||
f2995899ec | |||
5645e8fae1 | |||
a7ed5d09f1 | |||
3a91417db3 | |||
0844da594e | |||
64ad8931f3 | |||
a9d7b1c047 | |||
d6e4ea2e56 | |||
718414d924 | |||
943c30fa96 | |||
3d2c4c0fec | |||
8b8c2e076f | |||
0ee36dc429 | |||
48f5b3f71a | |||
ef819b0a2e | |||
91fc8a65a2 | |||
2d31eaa148 | |||
82e25ccef0 | |||
53a1662f70 | |||
dbb8684f68 | |||
6a4f82611b | |||
e6a7070589 | |||
e6588b4f5b | |||
20e723f39f | |||
a8729930e6 | |||
c725f14bf2 | |||
9b5216431d | |||
1d5c00768c | |||
7f1c9ec9a8 | |||
fe0a4d5991 | |||
f06ff83dce | |||
251aa6cf97 |
142
.drone.yml
142
.drone.yml
@ -1,88 +1,68 @@
|
|||||||
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
name: default
|
type: kubernetes
|
||||||
|
name: build
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/audit: privileged
|
||||||
|
|
||||||
|
services:
|
||||||
|
- name: docker daemon
|
||||||
|
image: docker:dind
|
||||||
|
privileged: true
|
||||||
|
environment:
|
||||||
|
DOCKER_TLS_CERTDIR: ""
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: release
|
- name: container build
|
||||||
image: plugins/gitea-release
|
image: docker:latest
|
||||||
settings:
|
privileged: true
|
||||||
api_key:
|
commands:
|
||||||
from_secret: gitea_api_key
|
- "while ! docker stats --no-stream; do sleep 1; done"
|
||||||
base_url: https://code.garrettmills.dev
|
- "docker build -t $DOCKER_REGISTRY/starship/coreid ."
|
||||||
checksum: md5
|
- "docker push $DOCKER_REGISTRY/starship/coreid"
|
||||||
title: ${DRONE_TAG}
|
environment:
|
||||||
|
DOCKER_HOST: tcp://localhost:2375
|
||||||
|
DOCKER_REGISTRY:
|
||||||
|
from_secret: DOCKER_REGISTRY
|
||||||
|
|
||||||
|
- name: environment substitution
|
||||||
|
image: rockylinux:9.0-minimal
|
||||||
|
commands:
|
||||||
|
- microdnf install -y gettext
|
||||||
|
- cd deploy && mkdir ../deploy-subst && bash -c 'for f in *.yaml; do envsubst < $f > ../deploy-subst/$f; done'
|
||||||
|
environment:
|
||||||
|
COREID_DOMAIN:
|
||||||
|
from_secret: COREID_DOMAIN
|
||||||
|
DOCKER_REGISTRY:
|
||||||
|
from_secret: DOCKER_REGISTRY
|
||||||
|
COREID_DATABASE_HOST:
|
||||||
|
from_secret: COREID_DATABASE_HOST
|
||||||
|
COREID_DATABASE_NAME:
|
||||||
|
from_secret: COREID_DATABASE_NAME
|
||||||
|
COREID_LDAP_BASE_DC:
|
||||||
|
from_secret: COREID_LDAP_BASE_DC
|
||||||
|
COREID_REDIS_HOST:
|
||||||
|
from_secret: COREID_REDIS_HOST
|
||||||
|
COREID_SMTP_HOST:
|
||||||
|
from_secret: COREID_SMTP_HOST
|
||||||
when:
|
when:
|
||||||
event: tag
|
|
||||||
- name: deploy to production
|
|
||||||
image: appleboy/drone-ssh
|
|
||||||
settings:
|
|
||||||
host:
|
|
||||||
from_secret: deploy_ssh_host
|
|
||||||
username:
|
|
||||||
from_secret: deploy_ssh_user
|
|
||||||
key:
|
|
||||||
from_secret: deploy_ssh_key
|
|
||||||
port:
|
|
||||||
from_secret: deploy_ssh_port
|
|
||||||
script:
|
|
||||||
- cd /home/coreid/CoreID
|
|
||||||
- git 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:
|
event:
|
||||||
- tag
|
- tag
|
||||||
- promote
|
- promote
|
||||||
|
|
||||||
|
- name: k8s rollout
|
||||||
|
image: bitnami/kubectl
|
||||||
|
privileged: true
|
||||||
|
commands:
|
||||||
|
- cd deploy-subst && kubectl apply -f .
|
||||||
|
- kubectl rollout restart deployment/coreid-www -n starship
|
||||||
|
- kubectl rollout restart deployment/coreid-jobs -n starship
|
||||||
|
depends_on:
|
||||||
|
- container build
|
||||||
|
- environment substitution
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
- promote
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
|
*.conf
|
||||||
|
|
||||||
# ---> Node
|
# ---> Node
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
@ -150,3 +152,4 @@ tmp.uploads/*
|
|||||||
!tmp.uploads/.gitkeep
|
!tmp.uploads/.gitkeep
|
||||||
uploads/*
|
uploads/*
|
||||||
!uploads/.gitkeep
|
!uploads/.gitkeep
|
||||||
|
ttls-pap.conf
|
||||||
|
16
Dockerfile
Normal file
16
Dockerfile
Normal 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"]
|
@ -44,6 +44,7 @@ const FlitterUnits = {
|
|||||||
'LDAPController': require('./app/unit/LDAPControllerUnit'),
|
'LDAPController': require('./app/unit/LDAPControllerUnit'),
|
||||||
'LDAPRoutingUnit': require('./app/unit/LDAPRoutingUnit'),
|
'LDAPRoutingUnit': require('./app/unit/LDAPRoutingUnit'),
|
||||||
'OpenIDConnect' : require('./app/unit/OpenIDConnectUnit'),
|
'OpenIDConnect' : require('./app/unit/OpenIDConnectUnit'),
|
||||||
|
'Radius' : require('./app/unit/RadiusUnit'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The Core Flitter Units
|
* The Core Flitter Units
|
||||||
|
BIN
app/assets/9101b9da062601235e3b9169706aa12a.png
Normal file
BIN
app/assets/9101b9da062601235e3b9169706aa12a.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
@ -38,14 +38,18 @@ export default class MFAChallengePage extends Component {
|
|||||||
static get props() { return ['app_name'] }
|
static get props() { return ['app_name'] }
|
||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
|
|
||||||
loading = false
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
verify_code = ''
|
this.loading = false
|
||||||
verify_success = false
|
|
||||||
|
|
||||||
error_message = ''
|
this.verify_code = ''
|
||||||
other_message = ''
|
this.verify_success = false
|
||||||
t = {}
|
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = ''
|
||||||
|
this.t = {}
|
||||||
|
}
|
||||||
|
|
||||||
async vue_on_create() {
|
async vue_on_create() {
|
||||||
this.t = await T(
|
this.t = await T(
|
||||||
|
@ -28,12 +28,16 @@ export default class MFADisableComponent extends Component {
|
|||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get props() { return [] }
|
static get props() { return [] }
|
||||||
|
|
||||||
app_name = ''
|
constructor() {
|
||||||
step = 0
|
super()
|
||||||
loading = false
|
|
||||||
error_message = ''
|
this.app_name = ''
|
||||||
other_message = ''
|
this.step = 0
|
||||||
t = {}
|
this.loading = false
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = ''
|
||||||
|
this.t = {}
|
||||||
|
}
|
||||||
|
|
||||||
async vue_on_create() {
|
async vue_on_create() {
|
||||||
this.app_name = session.get('app.name')
|
this.app_name = session.get('app.name')
|
||||||
|
@ -38,12 +38,16 @@ export default class MFARecoveryComponent extends Component {
|
|||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get props() { return ['app_name'] }
|
static get props() { return ['app_name'] }
|
||||||
|
|
||||||
verify_success = false
|
constructor() {
|
||||||
loading = false
|
super()
|
||||||
recovery_code = ''
|
|
||||||
error_message = ''
|
this.verify_success = false
|
||||||
other_message = ''
|
this.loading = false
|
||||||
t = {}
|
this.recovery_code = ''
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = ''
|
||||||
|
this.t = {}
|
||||||
|
}
|
||||||
|
|
||||||
async vue_on_create() {
|
async vue_on_create() {
|
||||||
this.t = await T(
|
this.t = await T(
|
||||||
|
@ -61,19 +61,23 @@ export default class MFASetupPage extends Component {
|
|||||||
static get props() { return ['app_name'] }
|
static get props() { return ['app_name'] }
|
||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
|
|
||||||
loading = false
|
constructor() {
|
||||||
step = 0
|
super()
|
||||||
|
|
||||||
qr_data = ''
|
this.loading = false
|
||||||
otpauth_url = ''
|
this.step = 0
|
||||||
secret = ''
|
|
||||||
verify_code = ''
|
|
||||||
|
|
||||||
verify_success = false
|
this.qr_data = ''
|
||||||
|
this.otpauth_url = ''
|
||||||
|
this.secret = ''
|
||||||
|
this.verify_code = ''
|
||||||
|
|
||||||
error_message = ''
|
this.verify_success = false
|
||||||
other_message = ''
|
|
||||||
t = {}
|
this.error_message = ''
|
||||||
|
this.other_message = ''
|
||||||
|
this.t = {}
|
||||||
|
}
|
||||||
|
|
||||||
async vue_on_create() {
|
async vue_on_create() {
|
||||||
this.t = await T(
|
this.t = await T(
|
||||||
|
@ -25,7 +25,11 @@ export default class AuthPage extends Component {
|
|||||||
static get props() { return ['app_name', 'message', 'actions'] }
|
static get props() { return ['app_name', 'message', 'actions'] }
|
||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
|
|
||||||
loading = false
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
async action_click(index) {
|
async action_click(index) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
@ -78,23 +78,27 @@ export default class PasswordResetComponent extends Component {
|
|||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get props() { return ['app_name'] }
|
static get props() { return ['app_name'] }
|
||||||
|
|
||||||
step = 0
|
constructor() {
|
||||||
loading = false
|
super()
|
||||||
has_mfa = false
|
|
||||||
|
|
||||||
error_message = ''
|
this.step = 0
|
||||||
other_message = ''
|
this.loading = false
|
||||||
|
this.has_mfa = false
|
||||||
|
|
||||||
step_1_valid = false
|
this.error_message = ''
|
||||||
step_1_calc_time = ''
|
this.other_message = ''
|
||||||
step_1_problem = ''
|
|
||||||
|
|
||||||
step_2_valid = false
|
this.step_1_valid = false
|
||||||
|
this.step_1_calc_time = ''
|
||||||
|
this.step_1_problem = ''
|
||||||
|
|
||||||
password = ''
|
this.step_2_valid = false
|
||||||
confirm_password = ''
|
|
||||||
t = {}
|
this.password = ''
|
||||||
ready = false
|
this.confirm_password = ''
|
||||||
|
this.t = {}
|
||||||
|
this.ready = false
|
||||||
|
}
|
||||||
|
|
||||||
async vue_on_create() {
|
async vue_on_create() {
|
||||||
this.has_mfa = !!session.get('user.has_mfa')
|
this.has_mfa = !!session.get('user.has_mfa')
|
||||||
|
@ -63,18 +63,21 @@ export default class AuthLoginForm extends Component {
|
|||||||
] }
|
] }
|
||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
|
|
||||||
username = ''
|
constructor() {
|
||||||
password = ''
|
super()
|
||||||
button_text = ''
|
|
||||||
step_two = false
|
|
||||||
btn_disabled = true
|
|
||||||
loading = false
|
|
||||||
error_message = ''
|
|
||||||
other_message = ''
|
|
||||||
allow_back = true
|
|
||||||
auth_user = false
|
|
||||||
|
|
||||||
t = {}
|
this.username = ''
|
||||||
|
this.password = ''
|
||||||
|
this.button_text = ''
|
||||||
|
this.step_two = false
|
||||||
|
this.btn_disabled = true
|
||||||
|
this.loading = false
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = ''
|
||||||
|
this.allow_back = true
|
||||||
|
this.auth_user = false
|
||||||
|
this.t = {}
|
||||||
|
}
|
||||||
|
|
||||||
watch_username(new_username, old_username) {
|
watch_username(new_username, old_username) {
|
||||||
this.btn_disabled = !new_username
|
this.btn_disabled = !new_username
|
||||||
|
@ -98,19 +98,23 @@ export default class RegistrationFormComponent extends Component {
|
|||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get props() { return ['app_name'] }
|
static get props() { return ['app_name'] }
|
||||||
|
|
||||||
loading = false
|
constructor() {
|
||||||
step = 1
|
super()
|
||||||
other_message = ''
|
|
||||||
error_message = ''
|
|
||||||
message = ''
|
|
||||||
btn_disabled = true
|
|
||||||
button_text = ''
|
|
||||||
|
|
||||||
first_name = ''
|
this.loading = false
|
||||||
last_name = ''
|
this.step = 1
|
||||||
username = ''
|
this.other_message = ''
|
||||||
email = ''
|
this.error_message = ''
|
||||||
t = {}
|
this.message = ''
|
||||||
|
this.btn_disabled = true
|
||||||
|
this.button_text = ''
|
||||||
|
|
||||||
|
this.first_name = ''
|
||||||
|
this.last_name = ''
|
||||||
|
this.username = ''
|
||||||
|
this.email = ''
|
||||||
|
this.t = {}
|
||||||
|
}
|
||||||
|
|
||||||
async vue_on_create() {
|
async vue_on_create() {
|
||||||
// Batch-load translated phrases
|
// Batch-load translated phrases
|
||||||
|
@ -28,7 +28,7 @@ const template = `
|
|||||||
v-if="field.type === 'display' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))"
|
v-if="field.type === 'display' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))"
|
||||||
v-html="typeof field.display === 'function' ? field.display(data) : field.display"
|
v-html="typeof field.display === 'function' ? field.display(data) : field.display"
|
||||||
></span>
|
></span>
|
||||||
<span v-if="field.type.startsWith('select') && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
<span v-if="field.type.startsWith('select') && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data, field.options))">
|
||||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||||
<select
|
<select
|
||||||
:id="uuid+field.field"
|
:id="uuid+field.field"
|
||||||
@ -42,13 +42,13 @@ const template = `
|
|||||||
<option
|
<option
|
||||||
v-for="option of field.options"
|
v-for="option of field.options"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
:selected="data[field.field] && data[field.field].includes(option.value)"
|
:selected="data[field.field] && (data[field.field] === option.value || (Array.isArray(data[field.field]) && data[field.field].includes(option.value)))"
|
||||||
>{{ typeof option.display === 'function' ? option.display(option) : option.display }}</option>
|
>{{ typeof option.display === 'function' ? option.display(option) : option.display }}</option>
|
||||||
</select>
|
</select>
|
||||||
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="field.type === 'text' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
<span v-if="field.type === 'text' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
||||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
<label :for="uuid+field.field" style="display: inline">{{ field.name }} <span v-if="field.help" :title="field.help"><img src="/assets/info-circle-solid.svg" height="18"></span></label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@ -146,20 +146,24 @@ export default class FormComponent extends Component {
|
|||||||
return ['resource', 'form_id', 'initial_mode']
|
return ['resource', 'form_id', 'initial_mode']
|
||||||
}
|
}
|
||||||
|
|
||||||
definition = {}
|
constructor() {
|
||||||
data = {}
|
super()
|
||||||
uuid = ''
|
|
||||||
title = ''
|
|
||||||
error_message = ''
|
|
||||||
other_message = ''
|
|
||||||
|
|
||||||
access_msg = ''
|
this.definition = {}
|
||||||
can_access = false
|
this.data = {}
|
||||||
|
this.uuid = ''
|
||||||
|
this.title = ''
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = ''
|
||||||
|
|
||||||
is_ready = false
|
this.access_msg = ''
|
||||||
mode = ''
|
this.can_access = false
|
||||||
id = ''
|
|
||||||
t = {}
|
this.is_ready = false
|
||||||
|
this.mode = ''
|
||||||
|
this.id = ''
|
||||||
|
this.t = {}
|
||||||
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.definition = {}
|
this.definition = {}
|
||||||
|
@ -65,13 +65,17 @@ export default class ListingComponent extends Component {
|
|||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get props() { return ['resource'] }
|
static get props() { return ['resource'] }
|
||||||
|
|
||||||
definition = {}
|
constructor() {
|
||||||
data = []
|
super()
|
||||||
resource_class = {}
|
|
||||||
|
|
||||||
access_msg = ''
|
this.definition = {}
|
||||||
can_access = false
|
this.data = []
|
||||||
t = {}
|
this.resource_class = {}
|
||||||
|
|
||||||
|
this.access_msg = ''
|
||||||
|
this.can_access = false
|
||||||
|
this.t = {}
|
||||||
|
}
|
||||||
|
|
||||||
async vue_on_create() {
|
async vue_on_create() {
|
||||||
this.t = await T(
|
this.t = await T(
|
||||||
|
@ -8,6 +8,8 @@ import AppSetupComponent from './dash/AppSetup.component.js'
|
|||||||
|
|
||||||
import ListingComponent from './cobalt/Listing.component.js'
|
import ListingComponent from './cobalt/Listing.component.js'
|
||||||
import FormComponent from './cobalt/Form.component.js'
|
import FormComponent from './cobalt/Form.component.js'
|
||||||
|
import RootPageComponent from './dash/RootPage.component.js'
|
||||||
|
import OutletComponent from './dash/Outlet.component.js'
|
||||||
|
|
||||||
import { T } from './service/Translate.service.js'
|
import { T } from './service/Translate.service.js'
|
||||||
|
|
||||||
@ -22,6 +24,8 @@ const dash_components = {
|
|||||||
|
|
||||||
ListingComponent,
|
ListingComponent,
|
||||||
FormComponent,
|
FormComponent,
|
||||||
|
RootPageComponent,
|
||||||
|
OutletComponent,
|
||||||
}
|
}
|
||||||
|
|
||||||
export { dash_components }
|
export { dash_components }
|
||||||
|
@ -232,35 +232,39 @@ export default class AppSetupComponent extends Component {
|
|||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get props() { return [] }
|
static get props() { return [] }
|
||||||
|
|
||||||
step = 0
|
constructor() {
|
||||||
btn_disabled = true
|
super()
|
||||||
btn_back = false
|
|
||||||
btn_hidden = false
|
|
||||||
btn_listing = false
|
|
||||||
|
|
||||||
name = ''
|
this.step = 0
|
||||||
identifier = ''
|
this.btn_disabled = true
|
||||||
type = '' // ldap | saml | oauth
|
this.btn_back = false
|
||||||
oauth_redirect_uri = ''
|
this.btn_hidden = false
|
||||||
|
this.btn_listing = false
|
||||||
|
|
||||||
saml_entity_id = ''
|
this.name = ''
|
||||||
saml_acs_url = ''
|
this.identifier = ''
|
||||||
saml_slo_url = ''
|
this.type = '' // ldap | saml | oauth
|
||||||
|
this.oauth_redirect_uri = ''
|
||||||
|
|
||||||
ldap_username = ''
|
this.saml_entity_id = ''
|
||||||
ldap_password = ''
|
this.saml_acs_url = ''
|
||||||
ldap_password_confirm = ''
|
this.saml_slo_url = ''
|
||||||
ldap_config = {}
|
|
||||||
|
|
||||||
error_message = ''
|
this.ldap_username = ''
|
||||||
|
this.ldap_password = ''
|
||||||
|
this.ldap_password_confirm = ''
|
||||||
|
this.ldap_config = {}
|
||||||
|
|
||||||
app = {}
|
this.error_message = ''
|
||||||
oauth_client = {}
|
|
||||||
saml_provider = {}
|
|
||||||
ldap_client = {}
|
|
||||||
|
|
||||||
app_name = ''
|
this.app = {}
|
||||||
host = ''
|
this.oauth_client = {}
|
||||||
|
this.saml_provider = {}
|
||||||
|
this.ldap_client = {}
|
||||||
|
|
||||||
|
this.app_name = ''
|
||||||
|
this.host = ''
|
||||||
|
}
|
||||||
|
|
||||||
make_url(path) {
|
make_url(path) {
|
||||||
return session.url(path)
|
return session.url(path)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component } from '../../lib/vues6/vues6.js'
|
import { Component } from '../../lib/vues6/vues6.js'
|
||||||
import { event_bus } from '../service/EventBus.service.js'
|
import { event_bus } from '../service/EventBus.service.js'
|
||||||
import { session } from '../service/Session.service.js'
|
import { session } from '../service/Session.service.js'
|
||||||
import { message_service } from '../service/Message.service.js'
|
import { action_service } from '../service/Action.service.js'
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
|
||||||
@ -36,9 +36,9 @@ const template = `
|
|||||||
aria-labelledby="navbarDropdown"
|
aria-labelledby="navbarDropdown"
|
||||||
>
|
>
|
||||||
<h6 class="dropdown-header">Hello, {{ first_name }}.</h6>
|
<h6 class="dropdown-header">Hello, {{ first_name }}.</h6>
|
||||||
<a href="/dash/profile" class="dropdown-item">My Profile</a>
|
<a href="/dash/profile" class="dropdown-item" @click="navigate('dash.profile')" onclick="return false;">My Profile</a>
|
||||||
<a href="/dash/c/listing/reflect/Token" v-if="can.api_tokens" class="dropdown-item">API Tokens</a>
|
<a href="/dash/c/listing/reflect/Token" v-if="can.api_tokens" @click="cobalt('reflect/Token', 'list')" class="dropdown-item" onclick="return false;">API Tokens</a>
|
||||||
<a href="/dash/c/listing/system/Announcement" v-if="can.messages" class="dropdown-item">System Announcements</a>
|
<a href="/dash/c/listing/system/Announcement" v-if="can.messages" @click="cobalt('system/Announcement', 'list')" class="dropdown-item" onclick="return false;">System Announcements</a>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a href="/auth/logout" class="dropdown-item">Sign-Out of {{ app_name }}</a>
|
<a href="/auth/logout" class="dropdown-item">Sign-Out of {{ app_name }}</a>
|
||||||
</div>
|
</div>
|
||||||
@ -53,10 +53,10 @@ export default class NavBarComponent extends Component {
|
|||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get props() { return [] }
|
static get props() { return [] }
|
||||||
|
|
||||||
can = {}
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
this.can = {}
|
||||||
this.toggle_event = event_bus.event('sidebar.toggle')
|
this.toggle_event = event_bus.event('sidebar.toggle')
|
||||||
this.first_name = session.get('user.first_name')
|
this.first_name = session.get('user.first_name')
|
||||||
this.last_name = session.get('user.last_name')
|
this.last_name = session.get('user.last_name')
|
||||||
@ -72,4 +72,20 @@ export default class NavBarComponent extends Component {
|
|||||||
toggle_sidebar() {
|
toggle_sidebar() {
|
||||||
this.toggle_event.fire()
|
this.toggle_event.fire()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigate(page) {
|
||||||
|
action_service.perform({
|
||||||
|
action: 'navigate',
|
||||||
|
page,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cobalt(resource, action, id = undefined) {
|
||||||
|
action_service.perform({
|
||||||
|
type: 'resource',
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
45
app/assets/app/dash/Outlet.component.js
Normal file
45
app/assets/app/dash/Outlet.component.js
Normal 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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
32
app/assets/app/dash/RootPage.component.js
Normal file
32
app/assets/app/dash/RootPage.component.js
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -23,13 +23,18 @@ export default class SideBarComponent extends Component {
|
|||||||
static get props() { return ['app_name'] }
|
static get props() { return ['app_name'] }
|
||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
|
|
||||||
actions = []
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
possible_actions = [
|
this.actions = []
|
||||||
|
|
||||||
|
this.isCollapsed = false
|
||||||
|
|
||||||
|
this.possible_actions = [
|
||||||
{
|
{
|
||||||
text: 'Profile',
|
text: 'Profile',
|
||||||
action: 'redirect',
|
action: 'navigate',
|
||||||
next: '/dash/profile',
|
page: 'dash.profile',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Users',
|
text: 'Users',
|
||||||
@ -55,6 +60,24 @@ export default class SideBarComponent extends Component {
|
|||||||
type: 'resource',
|
type: 'resource',
|
||||||
resource: 'iam/Policy',
|
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',
|
text: 'LDAP Clients',
|
||||||
action: 'list',
|
action: 'list',
|
||||||
@ -67,6 +90,12 @@ export default class SideBarComponent extends Component {
|
|||||||
type: 'resource',
|
type: 'resource',
|
||||||
resource: 'oauth/Client',
|
resource: 'oauth/Client',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'RADIUS Clients',
|
||||||
|
action: 'list',
|
||||||
|
type: 'resource',
|
||||||
|
resource: 'radius/Client',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'OpenID Connect Clients',
|
text: 'OpenID Connect Clients',
|
||||||
action: 'list',
|
action: 'list',
|
||||||
@ -87,8 +116,6 @@ export default class SideBarComponent extends Component {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
event_bus.event('sidebar.toggle').subscribe(() => {
|
event_bus.event('sidebar.toggle').subscribe(() => {
|
||||||
this.toggle()
|
this.toggle()
|
||||||
})
|
})
|
||||||
@ -120,8 +147,6 @@ export default class SideBarComponent extends Component {
|
|||||||
this.actions = new_actions
|
this.actions = new_actions
|
||||||
}
|
}
|
||||||
|
|
||||||
isCollapsed = false
|
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.isCollapsed = !this.isCollapsed
|
this.isCollapsed = !this.isCollapsed
|
||||||
}
|
}
|
||||||
|
@ -68,8 +68,12 @@ export default class MessageContainerComponent extends Component {
|
|||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get props() { return [] }
|
static get props() { return [] }
|
||||||
|
|
||||||
messages = []
|
constructor() {
|
||||||
modals = []
|
super()
|
||||||
|
|
||||||
|
this.messages = []
|
||||||
|
this.modals = []
|
||||||
|
}
|
||||||
|
|
||||||
vue_on_create() {
|
vue_on_create() {
|
||||||
this.alert_event = event_bus.event('message.alert')
|
this.alert_event = event_bus.event('message.alert')
|
||||||
|
@ -8,7 +8,7 @@ import { utility } from '../../service/Utility.service.js'
|
|||||||
import { profile_service } from '../../service/Profile.service.js'
|
import { profile_service } from '../../service/Profile.service.js'
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<div class="coreid-profile-container mb-5">
|
<div class="coreid-profile-container mb-5 offset-0 col-md-8 offset-md-2 col-xl-6 offset-xl-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -77,6 +77,20 @@ const template = `
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<h4 style="margin-left: 15px">{{ t['profile.advanced_header'] }}</h4>
|
||||||
|
<div class="col-12 form-group">
|
||||||
|
<label for="coreid-profile-shell-input">{{ t['profile.advanced_shell'] }}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="coreid-profile-shell-input"
|
||||||
|
v-model="profile_shell"
|
||||||
|
@keyup="on_key_up($event)"
|
||||||
|
placeholder="/bin/bash"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item text-right font-italic text-muted">
|
<li class="list-group-item text-right font-italic text-muted">
|
||||||
{{ form_message }}
|
{{ form_message }}
|
||||||
@ -90,6 +104,11 @@ const template = `
|
|||||||
@click="change_password"
|
@click="change_password"
|
||||||
>{{ t['password.change'] }}</button>
|
>{{ t['password.change'] }}</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<h4>{{ t['authn.authn'] }}</h4>
|
||||||
|
<p>{{ t['authn.desc'].replace(/APP_NAME/g, app_name) }}</p>
|
||||||
|
<button class="btn btn-success btn-sm" type="button">{{ t['authn.enable'] }}</button>
|
||||||
|
</li>
|
||||||
<li class="list-group-item" v-if="ready && !has_mfa && (!user_id || user_id === 'me')">
|
<li class="list-group-item" v-if="ready && !has_mfa && (!user_id || user_id === 'me')">
|
||||||
<h4>{{ t['mfa.mfa'] }}</h4>
|
<h4>{{ t['mfa.mfa'] }}</h4>
|
||||||
<p>{{ t['profile.mfa_1'].replace(/APP_NAME/g, app_name) }}</p>
|
<p>{{ t['profile.mfa_1'].replace(/APP_NAME/g, app_name) }}</p>
|
||||||
@ -117,6 +136,7 @@ const template = `
|
|||||||
<div class="col-9">
|
<div class="col-9">
|
||||||
{{ pw.name }}
|
{{ pw.name }}
|
||||||
<br><span class="text-muted font-italic">{{ t['profile.issued'] }} {{ pw.created }}</span>
|
<br><span class="text-muted font-italic">{{ t['profile.issued'] }} {{ pw.created }}</span>
|
||||||
|
<span class="text-muted font-italic"> | {{ t['profile.accessed'] }} {{ pw.accessed || t['common.never'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3 my-auto">
|
<div class="col-3 my-auto">
|
||||||
<button
|
<button
|
||||||
@ -195,31 +215,36 @@ export default class EditProfileComponent extends Component {
|
|||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get props() { return ['user_id'] }
|
static get props() { return ['user_id'] }
|
||||||
|
|
||||||
profile_first = ''
|
constructor() {
|
||||||
profile_last = ''
|
super()
|
||||||
profile_email = ''
|
|
||||||
profile_tagline = ''
|
|
||||||
last_reset = ''
|
|
||||||
mfa_enable_date = ''
|
|
||||||
|
|
||||||
has_mfa_recovery = false
|
this.profile_first = ''
|
||||||
mfa_recovery_date = ''
|
this.profile_last = ''
|
||||||
mfa_recovery_codes = 0
|
this.profile_email = ''
|
||||||
|
this.profile_tagline = ''
|
||||||
|
this.profile_shell = ''
|
||||||
|
this.last_reset = ''
|
||||||
|
this.mfa_enable_date = ''
|
||||||
|
|
||||||
form_message = 'No changes.'
|
this.has_mfa_recovery = false
|
||||||
|
this.mfa_recovery_date = ''
|
||||||
|
this.mfa_recovery_codes = 0
|
||||||
|
|
||||||
has_mfa = false
|
this.form_message = 'No changes.'
|
||||||
ready = false
|
|
||||||
|
|
||||||
notify_gateway_url = ''
|
this.has_mfa = false
|
||||||
notify_app_key = ''
|
this.ready = false
|
||||||
notify_enabled = false
|
|
||||||
notify_created_on = ''
|
|
||||||
notify_loaded = false
|
|
||||||
|
|
||||||
app_passwords = []
|
this.notify_gateway_url = ''
|
||||||
app_name = ''
|
this.notify_app_key = ''
|
||||||
t = {}
|
this.notify_enabled = false
|
||||||
|
this.notify_created_on = ''
|
||||||
|
this.notify_loaded = false
|
||||||
|
|
||||||
|
this.app_passwords = []
|
||||||
|
this.app_name = ''
|
||||||
|
this.t = {}
|
||||||
|
}
|
||||||
|
|
||||||
on_key_up = ($event) => {}
|
on_key_up = ($event) => {}
|
||||||
|
|
||||||
@ -267,7 +292,14 @@ export default class EditProfileComponent extends Component {
|
|||||||
'profile.app_key',
|
'profile.app_key',
|
||||||
'profile.example_gateway_url',
|
'profile.example_gateway_url',
|
||||||
'profile.save_notify',
|
'profile.save_notify',
|
||||||
'profile.test_notify'
|
'profile.test_notify',
|
||||||
|
'profile.advanced_header',
|
||||||
|
'profile.advanced_shell',
|
||||||
|
'profile.accessed',
|
||||||
|
'common.never',
|
||||||
|
'authn.authn',
|
||||||
|
'authn.desc',
|
||||||
|
'authn.enable',
|
||||||
)
|
)
|
||||||
|
|
||||||
this.app_name = session.get('app.name')
|
this.app_name = session.get('app.name')
|
||||||
@ -288,6 +320,7 @@ export default class EditProfileComponent extends Component {
|
|||||||
last_name: this.profile_last,
|
last_name: this.profile_last,
|
||||||
email: this.profile_email,
|
email: this.profile_email,
|
||||||
tagline: this.profile_tagline,
|
tagline: this.profile_tagline,
|
||||||
|
login_shell: this.profile_shell,
|
||||||
user_id: this.user_id || 'me',
|
user_id: this.user_id || 'me',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -336,6 +369,7 @@ export default class EditProfileComponent extends Component {
|
|||||||
this.profile_last = result.last_name
|
this.profile_last = result.last_name
|
||||||
this.profile_email = result.email
|
this.profile_email = result.email
|
||||||
this.profile_tagline = result.tagline
|
this.profile_tagline = result.tagline
|
||||||
|
this.profile_shell = result.login_shell
|
||||||
|
|
||||||
const notify_config = await profile_service.get_notify(this.user_id || 'me')
|
const notify_config = await profile_service.get_notify(this.user_id || 'me')
|
||||||
if ( !notify_config || !notify_config.has_config ) {
|
if ( !notify_config || !notify_config.has_config ) {
|
||||||
@ -380,6 +414,7 @@ export default class EditProfileComponent extends Component {
|
|||||||
this.app_passwords = app_pws.map(x => {
|
this.app_passwords = app_pws.map(x => {
|
||||||
if ( x.expires ) x.expires = (new Date(x.expires)).toLocaleDateString()
|
if ( x.expires ) x.expires = (new Date(x.expires)).toLocaleDateString()
|
||||||
if ( x.created ) x.created = (new Date(x.created)).toLocaleDateString()
|
if ( x.created ) x.created = (new Date(x.created)).toLocaleDateString()
|
||||||
|
if ( x.accessed ) x.accessed = (new Date(x.accessed)).toLocaleDateString()
|
||||||
return x
|
return x
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -72,12 +72,16 @@ export default class AppPasswordFormComponent extends Component {
|
|||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get props() { return [] }
|
static get props() { return [] }
|
||||||
|
|
||||||
name = ''
|
constructor() {
|
||||||
valid = false
|
super()
|
||||||
uuid = ''
|
|
||||||
enable_form = true
|
this.name = ''
|
||||||
display_password = ''
|
this.valid = false
|
||||||
t = {}
|
this.uuid = ''
|
||||||
|
this.enable_form = true
|
||||||
|
this.display_password = ''
|
||||||
|
this.t = {}
|
||||||
|
}
|
||||||
|
|
||||||
async vue_on_create() {
|
async vue_on_create() {
|
||||||
this.t = await T(
|
this.t = await T(
|
||||||
|
@ -29,8 +29,12 @@ export default class ProfilePhotoUploaderComponent extends Component {
|
|||||||
static get template() { return template }
|
static get template() { return template }
|
||||||
static get params() { return [] }
|
static get params() { return [] }
|
||||||
|
|
||||||
ready = false
|
constructor() {
|
||||||
t = {}
|
super()
|
||||||
|
|
||||||
|
this.ready = false
|
||||||
|
this.t = {}
|
||||||
|
}
|
||||||
|
|
||||||
async vue_on_create() {
|
async vue_on_create() {
|
||||||
this.t = await T(
|
this.t = await T(
|
||||||
|
@ -2,14 +2,17 @@ import CRUDBase from './CRUDBase.js'
|
|||||||
import { session } from '../service/Session.service.js'
|
import { session } from '../service/Session.service.js'
|
||||||
|
|
||||||
class AppResource extends CRUDBase {
|
class AppResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/applications'
|
constructor() {
|
||||||
required_fields = ['name', 'identifier']
|
super()
|
||||||
permission_base = 'v1:applications'
|
|
||||||
|
|
||||||
item = 'Application'
|
this.endpoint = '/api/v1/applications'
|
||||||
plural = 'Applications'
|
this.required_fields = ['name', 'identifier']
|
||||||
|
this.permission_base = 'v1:applications'
|
||||||
|
|
||||||
listing_definition = {
|
this.item = 'Application'
|
||||||
|
this.plural = 'Applications'
|
||||||
|
|
||||||
|
this.listing_definition = {
|
||||||
display: `
|
display: `
|
||||||
An application is anything that can authenticate users against ${session.get('app.name')}. Applications can have any number of associated LDAP clients, SAML service providers, and OAuth2 clients.
|
An application is anything that can authenticate users against ${session.get('app.name')}. Applications can have any number of associated LDAP clients, SAML service providers, and OAuth2 clients.
|
||||||
`,
|
`,
|
||||||
@ -37,10 +40,10 @@ class AppResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
position: 'main',
|
position: 'main',
|
||||||
action: 'redirect',
|
action: 'navigate',
|
||||||
text: 'Setup Wizard',
|
text: 'Setup Wizard',
|
||||||
color: 'success',
|
color: 'success',
|
||||||
next: '/dash/app/setup',
|
page: 'app.setup',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'resource',
|
type: 'resource',
|
||||||
@ -60,7 +63,7 @@ class AppResource extends CRUDBase {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
form_definition = {
|
this.form_definition = {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@ -81,6 +84,14 @@ class AppResource extends CRUDBase {
|
|||||||
field: 'description',
|
field: 'description',
|
||||||
type: 'textarea',
|
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',
|
name: 'Associated LDAP Clients',
|
||||||
field: 'ldap_client_ids',
|
field: 'ldap_client_ids',
|
||||||
@ -101,6 +112,16 @@ class AppResource extends CRUDBase {
|
|||||||
value: 'id',
|
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',
|
name: 'Associated OpenID Connect Clients',
|
||||||
field: 'openid_client_ids',
|
field: 'openid_client_ids',
|
||||||
@ -123,6 +144,7 @@ class AppResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = new AppResource()
|
const app = new AppResource()
|
||||||
|
@ -2,15 +2,17 @@ import APIParseError from './APIParseError.js'
|
|||||||
import { session } from '../service/Session.service.js'
|
import { session } from '../service/Session.service.js'
|
||||||
|
|
||||||
export default class CRUDBase {
|
export default class CRUDBase {
|
||||||
endpoint = '/api/v1'
|
constructor() {
|
||||||
required_fields = []
|
this.endpoint = '/api/v1'
|
||||||
permission_base = ''
|
this.required_fields = []
|
||||||
|
this.permission_base = ''
|
||||||
|
|
||||||
listing_definition = {}
|
this.listing_definition = {}
|
||||||
form_definition = {}
|
this.form_definition = {}
|
||||||
|
|
||||||
item = ''
|
this.item = ''
|
||||||
plural = ''
|
this.plural = ''
|
||||||
|
}
|
||||||
|
|
||||||
async can(action) {
|
async can(action) {
|
||||||
return session.check_permissions(`${this.permission_base}:${action}`)
|
return session.check_permissions(`${this.permission_base}:${action}`)
|
||||||
|
@ -2,14 +2,17 @@ import CRUDBase from './CRUDBase.js'
|
|||||||
import { session } from '../service/Session.service.js'
|
import { session } from '../service/Session.service.js'
|
||||||
|
|
||||||
class SettingResource extends CRUDBase {
|
class SettingResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/settings'
|
constructor() {
|
||||||
required_fields = ['key', 'value']
|
super()
|
||||||
permission_base = 'v1:settings'
|
|
||||||
|
|
||||||
item = 'Setting'
|
this.endpoint = '/api/v1/settings'
|
||||||
plural = 'Settings'
|
this.required_fields = ['key', 'value']
|
||||||
|
this.permission_base = 'v1:settings'
|
||||||
|
|
||||||
listing_definition = {
|
this.item = 'Setting'
|
||||||
|
this.plural = 'Settings'
|
||||||
|
|
||||||
|
this.listing_definition = {
|
||||||
display: `
|
display: `
|
||||||
<p>These are advanced settings that allow you to tweak the way ${session.get('app.name')} behaves. Tweak them at your own risk.</p>
|
<p>These are advanced settings that allow you to tweak the way ${session.get('app.name')} behaves. Tweak them at your own risk.</p>
|
||||||
`,
|
`,
|
||||||
@ -35,7 +38,7 @@ class SettingResource extends CRUDBase {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
form_definition = {
|
this.form_definition = {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'Setting Key',
|
name: 'Setting Key',
|
||||||
@ -50,6 +53,7 @@ class SettingResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setting = new SettingResource()
|
const setting = new SettingResource()
|
||||||
|
@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
|
|||||||
import { session } from '../../service/Session.service.js'
|
import { session } from '../../service/Session.service.js'
|
||||||
|
|
||||||
class GroupResource extends CRUDBase {
|
class GroupResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/auth/groups'
|
constructor() {
|
||||||
required_fields = ['name']
|
super()
|
||||||
permission_base = 'v1:auth:groups'
|
|
||||||
|
|
||||||
item = 'Group'
|
this.endpoint = '/api/v1/auth/groups'
|
||||||
plural = 'Groups'
|
this.required_fields = ['name']
|
||||||
|
this.permission_base = 'v1:auth:groups'
|
||||||
|
|
||||||
listing_definition = {
|
this.item = 'Group'
|
||||||
|
this.plural = 'Groups'
|
||||||
|
|
||||||
|
this.listing_definition = {
|
||||||
display: `
|
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.
|
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: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@ -59,6 +62,15 @@ class GroupResource extends CRUDBase {
|
|||||||
required: true,
|
required: true,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
|
/*{
|
||||||
|
name: 'Superuser equivalent?',
|
||||||
|
field: 'grants_sudo',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{display: 'Yes', value: true},
|
||||||
|
{display: 'No', value: false},
|
||||||
|
],
|
||||||
|
},*/
|
||||||
{
|
{
|
||||||
name: 'Users',
|
name: 'Users',
|
||||||
field: 'user_ids',
|
field: 'user_ids',
|
||||||
@ -71,6 +83,7 @@ class GroupResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auth_group = new GroupResource()
|
const auth_group = new GroupResource()
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import CRUDBase from '../CRUDBase.js'
|
import CRUDBase from '../CRUDBase.js'
|
||||||
|
|
||||||
class RoleResource extends CRUDBase {
|
class RoleResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/auth/roles'
|
|
||||||
required_fields = ['role', 'permissions']
|
|
||||||
permission_base = 'v1:auth:roles'
|
|
||||||
|
|
||||||
item = 'Role'
|
constructor() {
|
||||||
plural = 'Roles'
|
super()
|
||||||
|
|
||||||
|
this.endpoint = '/api/v1/auth/roles'
|
||||||
|
this.required_fields = ['role', 'permissions']
|
||||||
|
this.permission_base = 'v1:auth:roles'
|
||||||
|
|
||||||
|
this.item = 'Role'
|
||||||
|
this.plural = 'Roles'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auth_role = new RoleResource()
|
const auth_role = new RoleResource()
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import CRUDBase from '../CRUDBase.js'
|
import CRUDBase from '../CRUDBase.js'
|
||||||
|
|
||||||
class TrapResource extends CRUDBase {
|
class TrapResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/auth/traps'
|
constructor() {
|
||||||
required_fields = ['name', 'trap', 'redirect_to']
|
super()
|
||||||
permission_base = 'v1:auth:traps'
|
|
||||||
|
|
||||||
item = 'Trap'
|
this.endpoint = '/api/v1/auth/traps'
|
||||||
plural = 'Traps'
|
this.required_fields = ['name', 'trap', 'redirect_to']
|
||||||
|
this.permission_base = 'v1:auth:traps'
|
||||||
|
|
||||||
|
this.item = 'Trap'
|
||||||
|
this.plural = 'Traps'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auth_trap = new TrapResource()
|
const auth_trap = new TrapResource()
|
||||||
|
@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
|
|||||||
import { session } from '../../service/Session.service.js'
|
import { session } from '../../service/Session.service.js'
|
||||||
|
|
||||||
class UserResource extends CRUDBase {
|
class UserResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/auth/users'
|
constructor() {
|
||||||
required_fields = ['uid', 'first_name', 'last_name', 'email']
|
super()
|
||||||
permission_base = 'v1:auth:users'
|
|
||||||
|
|
||||||
item = 'User'
|
this.endpoint = '/api/v1/auth/users'
|
||||||
plural = 'Users'
|
this.required_fields = ['uid', 'first_name', 'last_name', 'email']
|
||||||
|
this.permission_base = 'v1:auth:users'
|
||||||
|
|
||||||
listing_definition = {
|
this.item = 'User'
|
||||||
|
this.plural = 'Users'
|
||||||
|
|
||||||
|
this.listing_definition = {
|
||||||
display: `
|
display: `
|
||||||
Users can be assigned permissions and, if granted, can manage their ${session.get('app.name')} accounts from the Profile page, as well as login to the external applications they've been given access to.
|
Users can be assigned permissions and, if granted, can manage their ${session.get('app.name')} accounts from the Profile page, as well as login to the external applications they've been given access to.
|
||||||
`,
|
`,
|
||||||
@ -57,7 +60,7 @@ class UserResource extends CRUDBase {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
form_definition = {
|
this.form_definition = {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'First Name',
|
name: 'First Name',
|
||||||
@ -111,6 +114,7 @@ class UserResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auth_user = new UserResource()
|
const auth_user = new UserResource()
|
||||||
|
87
app/assets/app/resource/iam/Permission.resource.js
Normal file
87
app/assets/app/resource/iam/Permission.resource.js
Normal 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 }
|
@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
|
|||||||
import { session } from '../../service/Session.service.js'
|
import { session } from '../../service/Session.service.js'
|
||||||
|
|
||||||
class PolicyResource extends CRUDBase {
|
class PolicyResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/iam/policy'
|
constructor() {
|
||||||
required_fields = ['entity_id', 'entity_type', 'target_id', 'target_type', 'access_type']
|
super()
|
||||||
permission_base = 'v1:iam:policy'
|
|
||||||
|
|
||||||
item = 'IAM Policy'
|
this.endpoint = '/api/v1/iam/policy'
|
||||||
plural = 'IAM Policies'
|
this.required_fields = ['entity_id', 'entity_type', 'target_id', 'target_type', 'access_type']
|
||||||
|
this.permission_base = 'v1:iam:policy'
|
||||||
|
|
||||||
listing_definition = {
|
this.item = 'IAM Policy'
|
||||||
|
this.plural = 'IAM Policies'
|
||||||
|
|
||||||
|
this.listing_definition = {
|
||||||
display: `
|
display: `
|
||||||
Identity & Access Management (IAM) policies give you fine grained control over which ${session.get('app.name')} users and groups are allowed to access which applications.
|
Identity & Access Management (IAM) policies give you fine grained control over which ${session.get('app.name')} users and groups are allowed to access which applications.
|
||||||
<br><br>
|
<br><br>
|
||||||
@ -38,6 +41,11 @@ class PolicyResource extends CRUDBase {
|
|||||||
name: 'Target',
|
name: 'Target',
|
||||||
field: 'target_display',
|
field: 'target_display',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Permission',
|
||||||
|
field: 'permission',
|
||||||
|
renderer: permission => permission || '-',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@ -65,7 +73,7 @@ class PolicyResource extends CRUDBase {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
form_definition = {
|
this.form_definition = {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'Subject Type',
|
name: 'Subject Type',
|
||||||
@ -73,8 +81,8 @@ class PolicyResource extends CRUDBase {
|
|||||||
required: true,
|
required: true,
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ display: 'User', value: 'user' },
|
{display: 'User', value: 'user'},
|
||||||
{ display: 'Group', value: 'group' },
|
{display: 'Group', value: 'group'},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -107,8 +115,8 @@ class PolicyResource extends CRUDBase {
|
|||||||
required: true,
|
required: true,
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ display: '...is granted access to...', value: 'allow' },
|
{display: '...is granted access to...', value: 'allow'},
|
||||||
{ display: '...is denied access to...', value: 'deny' },
|
{display: '...is denied access to...', value: 'deny'},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -117,8 +125,10 @@ class PolicyResource extends CRUDBase {
|
|||||||
required: true,
|
required: true,
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ display: 'Application', value: 'application' },
|
{display: 'Application', value: 'application'},
|
||||||
{ display: 'API Scope', value: 'api_scope' },
|
{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'
|
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: {
|
/*handlers: {
|
||||||
insert: {
|
insert: {
|
||||||
@ -155,6 +253,7 @@ class PolicyResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
},*/
|
},*/
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const iam_policy = new PolicyResource()
|
const iam_policy = new PolicyResource()
|
||||||
|
@ -2,19 +2,18 @@ import CRUDBase from '../CRUDBase.js'
|
|||||||
import { session } from '../../service/Session.service.js'
|
import { session } from '../../service/Session.service.js'
|
||||||
|
|
||||||
class ClientResource extends CRUDBase {
|
class ClientResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/ldap/clients'
|
constructor() {
|
||||||
required_fields = ['name', 'uid', 'password']
|
super()
|
||||||
permission_base = 'v1:ldap:clients'
|
|
||||||
|
|
||||||
item = 'LDAP Client'
|
this.endpoint = '/api/v1/ldap/clients'
|
||||||
plural = 'LDAP Clients'
|
this.required_fields = ['name', 'uid', 'password']
|
||||||
|
this.permission_base = 'v1:ldap:clients'
|
||||||
|
|
||||||
async server_config() {
|
this.item = 'LDAP Client'
|
||||||
const results = await axios.get('/api/v1/ldap/config')
|
this.plural = 'LDAP Clients'
|
||||||
if ( results && results.data && results.data.data ) return results.data.data
|
|
||||||
}
|
|
||||||
|
|
||||||
listing_definition = {
|
|
||||||
|
this.listing_definition = {
|
||||||
display: `
|
display: `
|
||||||
LDAP Clients are special user accounts that external applications can use to bind to ${session.get('app.name')}'s built-in LDAP server to allow these applications to authenticate users.
|
LDAP Clients are special user accounts that external applications can use to bind to ${session.get('app.name')}'s built-in LDAP server to allow these applications to authenticate users.
|
||||||
<br><br>
|
<br><br>
|
||||||
@ -56,7 +55,7 @@ class ClientResource extends CRUDBase {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
form_definition = {
|
this.form_definition = {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'Provider Name',
|
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()
|
const ldap_client = new ClientResource()
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import CRUDBase from '../CRUDBase.js'
|
import CRUDBase from '../CRUDBase.js'
|
||||||
|
|
||||||
class GroupResource extends CRUDBase {
|
class GroupResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/ldap/groups'
|
constructor() {
|
||||||
required_fields = ['name', 'role']
|
super()
|
||||||
permission_base = 'v1:ldap:groups'
|
|
||||||
|
|
||||||
item = 'LDAP Group'
|
this.endpoint = '/api/v1/ldap/groups'
|
||||||
plural = 'LDAP Groups'
|
this.required_fields = ['name', 'role']
|
||||||
|
this.permission_base = 'v1:ldap:groups'
|
||||||
|
|
||||||
listing_definition = {
|
this.item = 'LDAP Group'
|
||||||
|
this.plural = 'LDAP Groups'
|
||||||
|
|
||||||
|
this.listing_definition = {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'Group Name',
|
name: 'Group Name',
|
||||||
@ -50,7 +53,7 @@ class GroupResource extends CRUDBase {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
form_definition = {
|
this.form_definition = {
|
||||||
// back_action: {
|
// back_action: {
|
||||||
// text: 'Back',
|
// text: 'Back',
|
||||||
// action: 'back',
|
// action: 'back',
|
||||||
@ -93,6 +96,7 @@ class GroupResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ldap_group = new GroupResource()
|
const ldap_group = new GroupResource()
|
||||||
|
108
app/assets/app/resource/ldap/Machine.resource.js
Normal file
108
app/assets/app/resource/ldap/Machine.resource.js
Normal 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 }
|
98
app/assets/app/resource/ldap/MachineGroup.resource.js
Normal file
98
app/assets/app/resource/ldap/MachineGroup.resource.js
Normal 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 }
|
@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
|
|||||||
import { session } from '../../service/Session.service.js';
|
import { session } from '../../service/Session.service.js';
|
||||||
|
|
||||||
class ClientResource extends CRUDBase {
|
class ClientResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/oauth/clients'
|
constructor() {
|
||||||
required_fields = ['name', 'redirect_url', 'api_scopes']
|
super()
|
||||||
permission_base = 'v1:oauth:clients'
|
|
||||||
|
|
||||||
item = 'OAuth2 Client'
|
this.endpoint = '/api/v1/oauth/clients'
|
||||||
plural = 'OAuth2 Clients'
|
this.required_fields = ['name', 'redirect_url', 'api_scopes']
|
||||||
|
this.permission_base = 'v1:oauth:clients'
|
||||||
|
|
||||||
listing_definition = {
|
this.item = 'OAuth2 Client'
|
||||||
|
this.plural = 'OAuth2 Clients'
|
||||||
|
|
||||||
|
this.listing_definition = {
|
||||||
display: `
|
display: `
|
||||||
OAuth2 clients are applications that support authentication over the OAuth2 protocol. This allows you to add a "Sign-In with XXX" button for ${session.get('app.name')} to the application in question. To do this, you need to create an OAuth2 client for that application, and provide the name, redirect URL, and API scopes.
|
OAuth2 clients are applications that support authentication over the OAuth2 protocol. This allows you to add a "Sign-In with XXX" button for ${session.get('app.name')} to the application in question. To do this, you need to create an OAuth2 client for that application, and provide the name, redirect URL, and API scopes.
|
||||||
<br><br>
|
<br><br>
|
||||||
@ -58,7 +61,7 @@ class ClientResource extends CRUDBase {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
form_definition = {
|
this.form_definition = {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'Client Name',
|
name: 'Client Name',
|
||||||
@ -101,6 +104,7 @@ class ClientResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const oauth_client = new ClientResource()
|
const oauth_client = new ClientResource()
|
||||||
|
@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
|
|||||||
import { session } from '../../service/Session.service.js'
|
import { session } from '../../service/Session.service.js'
|
||||||
|
|
||||||
class ClientResource extends CRUDBase {
|
class ClientResource extends CRUDBase {
|
||||||
endpoint = '/openid/clients'
|
constructor() {
|
||||||
required_fields = ['client_name', 'grant_types', 'redirect_uri']
|
super()
|
||||||
permission_base = 'v1:openid:clients'
|
|
||||||
|
|
||||||
item = 'OpenID Connect Client'
|
this.endpoint = '/openid/clients'
|
||||||
plural = 'OpenID Connect Clients'
|
this.required_fields = ['client_name', 'grant_types', 'redirect_uri']
|
||||||
|
this.permission_base = 'v1:openid:clients'
|
||||||
|
|
||||||
listing_definition = {
|
this.item = 'OpenID Connect Client'
|
||||||
|
this.plural = 'OpenID Connect Clients'
|
||||||
|
|
||||||
|
this.listing_definition = {
|
||||||
display: `
|
display: `
|
||||||
OpenID Connect clients are applications that support authentication over the OpenID Connect protocol. This allows you to add a "Sign-In with XXX" button for ${session.get('app.name')} to the application in question. To do this, the application need only comply with the OpenID standards.
|
OpenID Connect clients are applications that support authentication over the OpenID Connect protocol. This allows you to add a "Sign-In with XXX" button for ${session.get('app.name')} to the application in question. To do this, the application need only comply with the OpenID standards.
|
||||||
`,
|
`,
|
||||||
@ -49,7 +52,7 @@ class ClientResource extends CRUDBase {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
form_definition = {
|
this.form_definition = {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'Client Name',
|
name: 'Client Name',
|
||||||
@ -70,8 +73,8 @@ class ClientResource extends CRUDBase {
|
|||||||
field: 'grant_types',
|
field: 'grant_types',
|
||||||
type: 'select.multiple',
|
type: 'select.multiple',
|
||||||
options: [
|
options: [
|
||||||
{ display: 'Refresh Token', value: 'refresh_token' },
|
{display: 'Refresh Token', value: 'refresh_token'},
|
||||||
{ display: 'Authorization Code', value: 'authorization_code' },
|
{display: 'Authorization Code', value: 'authorization_code'},
|
||||||
],
|
],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@ -91,6 +94,7 @@ class ClientResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openid_client = new ClientResource()
|
const openid_client = new ClientResource()
|
||||||
|
71
app/assets/app/resource/radius/Client.resource.js
Normal file
71
app/assets/app/resource/radius/Client.resource.js
Normal 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 }
|
@ -1,12 +1,16 @@
|
|||||||
import CRUDBase from '../CRUDBase.js'
|
import CRUDBase from '../CRUDBase.js'
|
||||||
|
|
||||||
class ScopeResource extends CRUDBase {
|
class ScopeResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/reflect/scopes'
|
constructor() {
|
||||||
required_fields = ['scope']
|
super()
|
||||||
permission_base = 'v1:reflect:scopes'
|
|
||||||
|
|
||||||
item = 'API Scope'
|
this.endpoint = '/api/v1/reflect/scopes'
|
||||||
plural = 'API Scopes'
|
this.required_fields = ['scope']
|
||||||
|
this.permission_base = 'v1:reflect:scopes'
|
||||||
|
|
||||||
|
this.item = 'API Scope'
|
||||||
|
this.plural = 'API Scopes'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reflect_scope = new ScopeResource()
|
const reflect_scope = new ScopeResource()
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import CRUDBase from '../CRUDBase.js'
|
import CRUDBase from '../CRUDBase.js'
|
||||||
|
|
||||||
class TokenResource extends CRUDBase {
|
class TokenResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/reflect/tokens'
|
constructor() {
|
||||||
required_fields = ['client_id']
|
super()
|
||||||
permission_base = 'v1:reflect:tokens'
|
this.endpoint = '/api/v1/reflect/tokens'
|
||||||
|
this.required_fields = ['client_id']
|
||||||
|
this.permission_base = 'v1:reflect:tokens'
|
||||||
|
|
||||||
item = 'API Token'
|
this.item = 'API Token'
|
||||||
plural = 'API Tokens'
|
this.plural = 'API Tokens'
|
||||||
|
|
||||||
listing_definition = {
|
this.listing_definition = {
|
||||||
display: `
|
display: `
|
||||||
This allows you to create bearer tokens manually to allow for easier testing of the API. Notably, this is meant as a measure for testing and development, not for long term use.
|
This allows you to create bearer tokens manually to allow for easier testing of the API. Notably, this is meant as a measure for testing and development, not for long term use.
|
||||||
<br><br>
|
<br><br>
|
||||||
@ -54,7 +56,7 @@ class TokenResource extends CRUDBase {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
form_definition = {
|
this.form_definition = {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'Client',
|
name: 'Client',
|
||||||
@ -83,6 +85,7 @@ class TokenResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reflect_token = new TokenResource()
|
const reflect_token = new TokenResource()
|
||||||
|
@ -2,14 +2,17 @@ import CRUDBase from '../CRUDBase.js'
|
|||||||
import { session } from '../../service/Session.service.js'
|
import { session } from '../../service/Session.service.js'
|
||||||
|
|
||||||
class ProviderResource extends CRUDBase {
|
class ProviderResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/saml/providers'
|
constructor() {
|
||||||
required_fields = ['name', 'acs_url', 'entity_id']
|
super()
|
||||||
permission_base = 'v1:saml:providers'
|
|
||||||
|
|
||||||
item = 'SAML Service Provider'
|
this.endpoint = '/api/v1/saml/providers'
|
||||||
plural = 'SAML Service Providers'
|
this.required_fields = ['name', 'acs_url', 'entity_id']
|
||||||
|
this.permission_base = 'v1:saml:providers'
|
||||||
|
|
||||||
listing_definition = {
|
this.item = 'SAML Service Provider'
|
||||||
|
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.
|
display: `SAML Service Providers are applications that support external authentication to a SAML Identity Provider. In this case, ${session.get('app.name')} is the identity provider, so these external applications can authenticate against it.
|
||||||
<br><br>
|
<br><br>
|
||||||
To do this, you need to know the SAML service provider's entity ID, assertion consumer service URL, and single-logout URL (if supported).`,
|
To do this, you need to know the SAML service provider's entity ID, assertion consumer service URL, and single-logout URL (if supported).`,
|
||||||
@ -58,7 +61,7 @@ class ProviderResource extends CRUDBase {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
form_definition = {
|
this.form_definition = {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'Provider Name',
|
name: 'Provider Name',
|
||||||
@ -89,6 +92,7 @@ class ProviderResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saml_provider = new ProviderResource()
|
const saml_provider = new ProviderResource()
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import CRUDBase from '../CRUDBase.js'
|
import CRUDBase from '../CRUDBase.js'
|
||||||
|
|
||||||
class AnnouncementResource extends CRUDBase {
|
class AnnouncementResource extends CRUDBase {
|
||||||
endpoint = '/api/v1/system/announcements'
|
constructor() {
|
||||||
required_fields = ['user_ids', 'group_ids', 'title', 'message', 'type']
|
super()
|
||||||
permission_base = 'v1:system:announcements'
|
|
||||||
|
|
||||||
item = 'System Announcement'
|
this.endpoint = '/api/v1/system/announcements'
|
||||||
plural = 'System Announcements'
|
this.required_fields = ['user_ids', 'group_ids', 'title', 'message', 'type']
|
||||||
|
this.permission_base = 'v1:system:announcements'
|
||||||
|
|
||||||
listing_definition = {
|
this.item = 'System Announcement'
|
||||||
|
this.plural = 'System Announcements'
|
||||||
|
|
||||||
|
this.listing_definition = {
|
||||||
display: `
|
display: `
|
||||||
System announcements are administrative messages that you want all or some of your users to see. These messages can be delivered via e-mail, as a message after login, or as a system banner announcement.
|
System announcements are administrative messages that you want all or some of your users to see. These messages can be delivered via e-mail, as a message after login, or as a system banner announcement.
|
||||||
`,
|
`,
|
||||||
@ -42,7 +45,7 @@ class AnnouncementResource extends CRUDBase {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
form_definition = {
|
this.form_definition = {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'Title',
|
name: 'Title',
|
||||||
@ -79,9 +82,9 @@ class AnnouncementResource extends CRUDBase {
|
|||||||
field: 'type',
|
field: 'type',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ display: 'Login Intercept', value: 'login' },
|
{display: 'Login Intercept', value: 'login'},
|
||||||
{ display: 'E-Mail', value: 'email' },
|
{display: 'E-Mail', value: 'email'},
|
||||||
{ display: 'System Banner', value: 'banner' },
|
{display: 'System Banner', value: 'banner'},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -92,6 +95,7 @@ class AnnouncementResource extends CRUDBase {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const system_announcement = new AnnouncementResource()
|
const system_announcement = new AnnouncementResource()
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { location_service } from './Location.service.js'
|
import { location_service } from './Location.service.js'
|
||||||
import { resource_service } from './Resource.service.js'
|
import { resource_service } from './Resource.service.js'
|
||||||
|
import { event_bus } from './EventBus.service.js'
|
||||||
|
|
||||||
|
const pageMap = {
|
||||||
|
'dash.profile': '/dash/profile',
|
||||||
|
'app.setup': '/dash/app/setup',
|
||||||
|
}
|
||||||
|
|
||||||
class ActionService {
|
class ActionService {
|
||||||
async perform({ text = '', action, ...args }) {
|
async perform({ text = '', action, ...args }) {
|
||||||
@ -7,21 +13,44 @@ class ActionService {
|
|||||||
if ( args.next ) {
|
if ( args.next ) {
|
||||||
return location_service.redirect(args.next, args.delay || 0)
|
return location_service.redirect(args.next, args.delay || 0)
|
||||||
}
|
}
|
||||||
|
} else if ( action === 'navigate' ) {
|
||||||
|
if ( args.page && pageMap[args.page] ) {
|
||||||
|
window.history.pushState('pageNavigate', `Open ${args.page}`, pageMap[args.page])
|
||||||
|
return event_bus.event('root.navigate').fire(args)
|
||||||
|
}
|
||||||
} else if ( action === 'back' ) {
|
} else if ( action === 'back' ) {
|
||||||
return location_service.back()
|
return location_service.back()
|
||||||
} else if ( args.type === 'resource' ) {
|
} else if ( args.type === 'resource' ) {
|
||||||
const { resource } = args
|
const { resource } = args
|
||||||
if ( action === 'insert' ) {
|
if ( action === 'insert' ) {
|
||||||
return location_service.redirect(`/dash/c/form/${resource}`, 0)
|
window.history.pushState('cobaltForm', `Insert ${resource}`, `/dash/c/form/${resource}`)
|
||||||
|
|
||||||
|
return event_bus.event('root.navigate').fire({
|
||||||
|
page: 'cobalt.form',
|
||||||
|
resource,
|
||||||
|
mode: 'insert',
|
||||||
|
})
|
||||||
} else if ( action === 'update' ) {
|
} else if ( action === 'update' ) {
|
||||||
const { id } = args
|
const { id } = args
|
||||||
return location_service.redirect(`/dash/c/form/${resource}?id=${id}`, 0)
|
window.history.pushState('cobaltForm', `Edit ${resource}`, `/dash/c/form/${resource}?id=${id}`)
|
||||||
|
|
||||||
|
return event_bus.event('root.navigate').fire({
|
||||||
|
page: 'cobalt.form',
|
||||||
|
resource,
|
||||||
|
mode: 'update',
|
||||||
|
form_id: id,
|
||||||
|
})
|
||||||
} else if ( action === 'delete' ) {
|
} else if ( action === 'delete' ) {
|
||||||
const { id } = args
|
const { id } = args
|
||||||
const rsc = await resource_service.get(resource)
|
const rsc = await resource_service.get(resource)
|
||||||
await rsc.delete(id)
|
await rsc.delete(id)
|
||||||
} else if ( action === 'list' ) {
|
} else if ( action === 'list' ) {
|
||||||
return location_service.redirect(`/dash/c/listing/${resource}`, 0)
|
window.history.pushState('cobaltListing', `View ${resource}`, `/dash/c/listing/${resource}`)
|
||||||
|
|
||||||
|
return event_bus.event('root.navigate').fire({
|
||||||
|
page: 'cobalt.listing',
|
||||||
|
resource,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else if ( action === 'post' ) {
|
} else if ( action === 'post' ) {
|
||||||
const inputs = []
|
const inputs = []
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
class Event {
|
class Event {
|
||||||
firings = []
|
|
||||||
subscriptions = []
|
|
||||||
|
|
||||||
constructor(name) {
|
constructor(name) {
|
||||||
this.name = name
|
this.name = name
|
||||||
|
this.firings = []
|
||||||
|
this.subscriptions = []
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(handler) {
|
subscribe(handler) {
|
||||||
@ -22,7 +22,9 @@ class Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class EventBusService {
|
class EventBusService {
|
||||||
_events = {}
|
constructor() {
|
||||||
|
this._events = {}
|
||||||
|
}
|
||||||
|
|
||||||
event(name) {
|
event(name) {
|
||||||
if ( !this._events[name] ) {
|
if ( !this._events[name] ) {
|
||||||
|
@ -2,7 +2,9 @@ import { event_bus } from './EventBus.service.js'
|
|||||||
import { auth_api } from './AuthApi.service.js'
|
import { auth_api } from './AuthApi.service.js'
|
||||||
|
|
||||||
class MessageService {
|
class MessageService {
|
||||||
listener_interval = 25000
|
constructor() {
|
||||||
|
this.listener_interval = 25000
|
||||||
|
}
|
||||||
|
|
||||||
alert({type, message, timeout = 0, on_dismiss = () => {} }) {
|
alert({type, message, timeout = 0, on_dismiss = () => {} }) {
|
||||||
event_bus.event('message.alert').fire({ type, message, timeout, on_dismiss })
|
event_bus.event('message.alert').fire({ type, message, timeout, on_dismiss })
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import {message_service} from './Message.service.js'
|
||||||
|
|
||||||
class ProfileService {
|
class ProfileService {
|
||||||
|
|
||||||
async get_profile(user_id = 'me') {
|
async get_profile(user_id = 'me') {
|
||||||
@ -10,8 +12,11 @@ class ProfileService {
|
|||||||
if ( results && results.data && results.data.data ) return results.data.data
|
if ( results && results.data && results.data.data ) return results.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async update_profile({ user_id, first_name, last_name, email, tagline = undefined }) {
|
async update_profile({ user_id, first_name, last_name, email, login_shell = undefined, tagline = undefined }) {
|
||||||
await axios.patch(`/api/v1/profile/${user_id}`, { first_name, last_name, email, tagline })
|
const results = await axios.patch(`/api/v1/profile/${user_id}`, { first_name, last_name, email, tagline, login_shell })
|
||||||
|
if ( results && results.data && results.data.data && results.data.data.force_message_refresh ) {
|
||||||
|
await message_service._listener_tick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async update_notify({ user_id = 'me', app_key, gateway_url }) {
|
async update_notify({ user_id = 'me', app_key, gateway_url }) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
class Session {
|
class Session {
|
||||||
data = {}
|
constructor() {
|
||||||
|
this.data = {}
|
||||||
|
}
|
||||||
|
|
||||||
init(data) {
|
init(data) {
|
||||||
this.data = data
|
this.data = data
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
class TranslateService {
|
class TranslateService {
|
||||||
_cache = {}
|
constructor() {
|
||||||
|
this._cache = {}
|
||||||
|
}
|
||||||
|
|
||||||
check_cache(...keys) {
|
check_cache(...keys) {
|
||||||
const obj = {}
|
const obj = {}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
class UtilityService {
|
class UtilityService {
|
||||||
_debounce_timeouts = {}
|
constructor() {
|
||||||
|
this._debounce_timeouts = {}
|
||||||
|
}
|
||||||
|
|
||||||
uuid() {
|
uuid() {
|
||||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||||
|
1
app/assets/info-circle-solid.svg
Normal file
1
app/assets/info-circle-solid.svg
Normal 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 |
1
app/assets/lib/axios/axios.min.js
vendored
1
app/assets/lib/axios/axios.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/lib/popper/popper-1.16.0.min.js
vendored
1
app/assets/lib/popper/popper-1.16.0.min.js
vendored
File diff suppressed because one or more lines are too long
55
app/classes/radius/CoreIDAuthentication.js
Normal file
55
app/classes/radius/CoreIDAuthentication.js
Normal 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
|
28
app/classes/radius/CoreIDRadiusServer.mjs
Normal file
28
app/classes/radius/CoreIDRadiusServer.mjs
Normal 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?')
|
||||||
|
}
|
||||||
|
}
|
40
app/classes/radius/CoreIDUserPasswordPacketHandler.mjs
Normal file
40
app/classes/radius/CoreIDUserPasswordPacketHandler.mjs
Normal 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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -105,7 +105,7 @@ class OpenIDController extends Controller {
|
|||||||
const Client = this.models.get('openid:Client')
|
const Client = this.models.get('openid:Client')
|
||||||
const client = await Client.findById(req.params.id)
|
const client = await Client.findById(req.params.id)
|
||||||
|
|
||||||
if ( !client || !client.active )
|
if ( !client )
|
||||||
return res.status(404)
|
return res.status(404)
|
||||||
.message(req.T('api.client_not_found'))
|
.message(req.T('api.client_not_found'))
|
||||||
.api()
|
.api()
|
||||||
@ -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', {
|
return res.page('public:message', {
|
||||||
...this.Vue.data({
|
...this.Vue.data({
|
||||||
message: `<h3 class="font-weight-light">Authorize ${application.name}?</h3>
|
message: `<h3 class="font-weight-light">Authorize ${application.name}?</h3>
|
||||||
@ -170,6 +176,11 @@ class OpenIDController extends Controller {
|
|||||||
{
|
{
|
||||||
text: req.T('common.grant'),
|
text: req.T('common.grant'),
|
||||||
action: 'redirect',
|
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`,
|
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 }) {
|
async login(req, res, { uid, prompt, params, session }) {
|
||||||
return res.redirect(`/openid/interaction/${uid.toLowerCase()}/start-session`)
|
return res.redirect(`/openid/interaction/${uid.toLowerCase()}/start-session`)
|
||||||
}
|
}
|
||||||
|
@ -115,6 +115,28 @@ class AppController extends Controller {
|
|||||||
application.oauth_client_ids = oauth_client_ids
|
application.oauth_client_ids = oauth_client_ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify RADIUS client IDs
|
||||||
|
const RadiusClient = this.models.get('radius:Client')
|
||||||
|
if ( req.body.radius_client_ids ) {
|
||||||
|
const parsed = typeof req.body.radius_client_ids === 'string' ? this.utility.infer(req.body.radius_client_ids) : req.body.radius_client_ids
|
||||||
|
const radius_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||||
|
for ( const id of radius_client_ids ) {
|
||||||
|
const client = await RadiusClient.findById(id)
|
||||||
|
if ( !client || !client.active || !req.user.can(`radius:client:${client.id}:view`) )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('api.invalid_radius_client_id')} ${id}`)
|
||||||
|
.api()
|
||||||
|
|
||||||
|
const other_assoc_app = await Application.findOne({ radius_client_ids: client.id })
|
||||||
|
if ( other_assoc_app )
|
||||||
|
return res.status(400) // TODO translate this
|
||||||
|
.message(`The RADIUS client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
application.radius_client_ids = radius_client_ids
|
||||||
|
}
|
||||||
|
|
||||||
// Verify OpenID client IDs
|
// Verify OpenID client IDs
|
||||||
const OpenIDClient = this.models.get('openid:Client')
|
const OpenIDClient = this.models.get('openid:Client')
|
||||||
if ( req.body.openid_client_ids ) {
|
if ( req.body.openid_client_ids ) {
|
||||||
@ -242,6 +264,28 @@ class AppController extends Controller {
|
|||||||
application.oauth_client_ids = oauth_client_ids
|
application.oauth_client_ids = oauth_client_ids
|
||||||
} else application.oauth_client_ids = []
|
} else application.oauth_client_ids = []
|
||||||
|
|
||||||
|
// Verify OAuth client IDs
|
||||||
|
const RadiusClient = this.models.get('radius:Client')
|
||||||
|
if ( req.body.radius_client_ids ) {
|
||||||
|
const parsed = typeof req.body.radius_client_ids === 'string' ? this.utility.infer(req.body.radius_client_ids) : req.body.radius_client_ids
|
||||||
|
const radius_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||||
|
for ( const id of radius_client_ids ) {
|
||||||
|
const client = await RadiusClient.findById(id)
|
||||||
|
if ( !client || !client.active || !req.user.can(`radius:client:${client.id}:view`) )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('api.invalid_radius_client_id')} ${id}`)
|
||||||
|
.api()
|
||||||
|
|
||||||
|
const other_assoc_app = await Application.findOne({ radius_client_ids: client.id })
|
||||||
|
if ( other_assoc_app && other_assoc_app.id !== application.id )
|
||||||
|
return res.status(400) // TODO translate this
|
||||||
|
.message(`The RADIUS client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
application.radius_client_ids = radius_client_ids
|
||||||
|
} else application.radius_client_ids = []
|
||||||
|
|
||||||
// Verify OpenID client IDs
|
// Verify OpenID client IDs
|
||||||
const OpenIDClient = this.models.get('openid:Client')
|
const OpenIDClient = this.models.get('openid:Client')
|
||||||
if ( req.body.openid_client_ids ) {
|
if ( req.body.openid_client_ids ) {
|
||||||
|
@ -91,6 +91,7 @@ class AuthController extends Controller {
|
|||||||
if ( !(await User.findOne()) ) user.promote('root')
|
if ( !(await User.findOne()) ) user.promote('root')
|
||||||
|
|
||||||
await user.save()
|
await user.save()
|
||||||
|
await user.grant_defaults()
|
||||||
|
|
||||||
// Log in the user automatically
|
// Log in the user automatically
|
||||||
await this.auth.get_provider().session(req, user)
|
await this.auth.get_provider().session(req, user)
|
||||||
@ -219,6 +220,48 @@ class AuthController extends Controller {
|
|||||||
return res.api(await user.to_api())
|
return res.api(await user.to_api())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get_user_flat(req, res, next) {
|
||||||
|
if ( req.params.id === 'me' )
|
||||||
|
return res.json(await req.user.to_api())
|
||||||
|
|
||||||
|
const User = this.models.get('auth:User')
|
||||||
|
const user = await User.findById(req.params.id)
|
||||||
|
|
||||||
|
if ( !user )
|
||||||
|
return res.status(404)
|
||||||
|
.message(req.T('api.user_not_found'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
if ( !req.user.can(`auth:user:${user.id}:view`) )
|
||||||
|
return res.status(401)
|
||||||
|
.message(req.T('api.insufficient_permissions'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
return res.json(await user.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_user_photo(req, res, next) {
|
||||||
|
let user
|
||||||
|
if ( req.params.id === 'me' ) {
|
||||||
|
user = req.user
|
||||||
|
} else {
|
||||||
|
const User = this.models.get('auth:User')
|
||||||
|
user = await User.findOne({ uid: req.params.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !user )
|
||||||
|
return res.status(404)
|
||||||
|
.message(req.T('api.user_not_found'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
const file = await user.photo()
|
||||||
|
if ( !file )
|
||||||
|
// The user does not have a profile. Send the default.
|
||||||
|
return res.sendFile(this.utility.path('app/assets/people.png'))
|
||||||
|
|
||||||
|
await file.send(res)
|
||||||
|
}
|
||||||
|
|
||||||
async create_group(req, res, next) {
|
async create_group(req, res, next) {
|
||||||
if ( !req.user.can(`auth:group:create`) )
|
if ( !req.user.can(`auth:group:create`) )
|
||||||
return res.status(401)
|
return res.status(401)
|
||||||
@ -239,7 +282,10 @@ class AuthController extends Controller {
|
|||||||
.message(req.T('api.group_already_exists'))
|
.message(req.T('api.group_already_exists'))
|
||||||
.api()
|
.api()
|
||||||
|
|
||||||
const group = new Group({ name: req.body.name })
|
const group = new Group({
|
||||||
|
name: req.body.name,
|
||||||
|
grants_sudo: !!req.body.grants_sudo,
|
||||||
|
})
|
||||||
|
|
||||||
// Validate user ids
|
// Validate user ids
|
||||||
const User = this.models.get('auth:User')
|
const User = this.models.get('auth:User')
|
||||||
@ -258,6 +304,7 @@ class AuthController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await group.save()
|
await group.save()
|
||||||
|
await group.get_gid_number()
|
||||||
return res.api(await group.to_api())
|
return res.api(await group.to_api())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,6 +364,7 @@ class AuthController extends Controller {
|
|||||||
|
|
||||||
await user.reset_password(req.body.password, 'create')
|
await user.reset_password(req.body.password, 'create')
|
||||||
await user.save()
|
await user.save()
|
||||||
|
await user.grant_defaults()
|
||||||
return res.api(await user.to_api())
|
return res.api(await user.to_api())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,7 +413,10 @@ class AuthController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group.name = req.body.name
|
group.name = req.body.name
|
||||||
|
group.grants_sudo = !!req.body.grants_sudo
|
||||||
|
|
||||||
await group.save()
|
await group.save()
|
||||||
|
await group.get_gid_number()
|
||||||
return res.api()
|
return res.api()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ class IAMController extends Controller {
|
|||||||
.message(`${req.T('api.missing_field', true)} entity_id, target_id`)
|
.message(`${req.T('api.missing_field', true)} entity_id, target_id`)
|
||||||
.api()
|
.api()
|
||||||
|
|
||||||
return res.api(await Policy.check_entity_access(req.body.entity_id, req.body.target_id))
|
return res.api(await Policy.check_entity_access(req.body.entity_id, req.body.target_id, req.body.permission || undefined))
|
||||||
}
|
}
|
||||||
|
|
||||||
async check_user_access(req, res, next) {
|
async check_user_access(req, res, next) {
|
||||||
@ -39,7 +39,7 @@ class IAMController extends Controller {
|
|||||||
.message(req.T('api.insufficient_permissions'))
|
.message(req.T('api.insufficient_permissions'))
|
||||||
.api()
|
.api()
|
||||||
|
|
||||||
return res.api(await Policy.check_user_access(user, req.body.target_id))
|
return res.api(await Policy.check_user_access(user, req.body.target_id, req.body.permission || undefined))
|
||||||
}
|
}
|
||||||
|
|
||||||
async get_policies(req, res, next) {
|
async get_policies(req, res, next) {
|
||||||
@ -56,6 +56,33 @@ class IAMController extends Controller {
|
|||||||
return res.api(data)
|
return res.api(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get_permissions(req, res, next) {
|
||||||
|
const Permission = this.models.get('iam:Permission')
|
||||||
|
const permissions = await Permission.find({
|
||||||
|
active: true,
|
||||||
|
...(req.query.target_type ? {
|
||||||
|
target_type: req.query.target_type,
|
||||||
|
} : {})
|
||||||
|
})
|
||||||
|
const data = []
|
||||||
|
|
||||||
|
for ( const perm of permissions ) {
|
||||||
|
if ( req.user.can(`iam:permission:${perm.target_type}:view`) ) {
|
||||||
|
data.push(await perm.to_api())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( req.query.include_unset ) {
|
||||||
|
data.reverse().push({
|
||||||
|
permission: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
data.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.api(data)
|
||||||
|
}
|
||||||
|
|
||||||
async get_policy(req, res, next) {
|
async get_policy(req, res, next) {
|
||||||
const Policy = this.models.get('iam:Policy')
|
const Policy = this.models.get('iam:Policy')
|
||||||
const policy = await Policy.findById(req.params.id)
|
const policy = await Policy.findById(req.params.id)
|
||||||
@ -73,6 +100,23 @@ class IAMController extends Controller {
|
|||||||
return res.api(await policy.to_api())
|
return res.api(await policy.to_api())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get_permission(req, res, next) {
|
||||||
|
const Permission = this.models.get('iam:Permission')
|
||||||
|
const permission = await Permission.findById(req.params.id)
|
||||||
|
|
||||||
|
if ( !permission )
|
||||||
|
return res.status(404)
|
||||||
|
.message(req.T('iam.permission_not_found'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
if ( !req.user.can(`iam:permission:${permission.target_type}:view`) )
|
||||||
|
return res.status(401)
|
||||||
|
.message(req.T('api.insufficient_permissions'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
return res.api(await permission.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
async create_policy(req, res, next) {
|
async create_policy(req, res, next) {
|
||||||
const Policy = this.models.get('iam:Policy')
|
const Policy = this.models.get('iam:Policy')
|
||||||
|
|
||||||
@ -108,12 +152,12 @@ class IAMController extends Controller {
|
|||||||
|
|
||||||
if ( !['allow', 'deny'].includes(req.body.access_type) )
|
if ( !['allow', 'deny'].includes(req.body.access_type) )
|
||||||
return res.status(400)
|
return res.status(400)
|
||||||
.message(`${req.T('common.invalid')} access_type. ${req.T('api:must_one')} allow, deny.`)
|
.message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`)
|
||||||
.api()
|
.api()
|
||||||
|
|
||||||
if ( !['application', 'api_scope'].includes(req.body.target_type) )
|
if ( !['application', 'api_scope', 'machine', 'machine_group'].includes(req.body.target_type) )
|
||||||
return res.status(400)
|
return res.status(400)
|
||||||
.message(`${req.T('common.invalid')} target_type. ${req.T('api:must_one')} application, api_scope.`)
|
.message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope, machine, machine_group.`)
|
||||||
.api()
|
.api()
|
||||||
|
|
||||||
// Make sure the target_id is valid
|
// Make sure the target_id is valid
|
||||||
@ -130,6 +174,20 @@ class IAMController extends Controller {
|
|||||||
return res.status(400)
|
return res.status(400)
|
||||||
.message(`${req.T('common.invalid')} target_id.`)
|
.message(`${req.T('common.invalid')} target_id.`)
|
||||||
.api()
|
.api()
|
||||||
|
} else if ( req.body.target_type === 'machine' ) {
|
||||||
|
const Machine = this.models.get('ldap:Machine')
|
||||||
|
const machine = await Machine.findById(req.body.target_id)
|
||||||
|
if ( !machine || !machine.active || !req.user.can(`ldap:machine:${machine.id}:view`) )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('common.invalid')} target_id.`)
|
||||||
|
.api()
|
||||||
|
} else if ( req.body.target_type === 'machine_group' ) {
|
||||||
|
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||||
|
const group = await MachineGroup.findById(req.body.target_id)
|
||||||
|
if ( !group || !group.active || !req.user.can(`ldap:machine_group:${group.id}:view`) )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('common.invalid')} target_id.`)
|
||||||
|
.api()
|
||||||
}
|
}
|
||||||
|
|
||||||
const policy = new Policy({
|
const policy = new Policy({
|
||||||
@ -140,12 +198,71 @@ class IAMController extends Controller {
|
|||||||
target_id: req.body.target_id,
|
target_id: req.body.target_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if ( req.body.permission ) {
|
||||||
|
// Validate the permission and set it, if it is valid
|
||||||
|
const Permission = this.models.get('iam:Permission')
|
||||||
|
const permission = await Permission.findOne({
|
||||||
|
active: true,
|
||||||
|
target_type: req.body.target_type,
|
||||||
|
permission: req.body.permission,
|
||||||
|
})
|
||||||
|
|
||||||
|
if ( permission ) {
|
||||||
|
policy.for_permission = true
|
||||||
|
policy.permission = req.body.permission
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await policy.save()
|
await policy.save()
|
||||||
req.user.allow(`iam:policy:${policy.id}`)
|
req.user.allow(`iam:policy:${policy.id}`)
|
||||||
await req.user.save()
|
await req.user.save()
|
||||||
return res.api(await policy.to_api())
|
return res.api(await policy.to_api())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async create_permission(req, res, next) {
|
||||||
|
const Permission = this.models.get('iam:Permission')
|
||||||
|
|
||||||
|
const required_fields = ['target_type', 'permission']
|
||||||
|
for ( const field of required_fields ) {
|
||||||
|
if ( !req.body[field] )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('api.missing_field')} ${field}`)
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group']
|
||||||
|
if ( !valid_target_types.includes(req.body.target_type) ) {
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('api.invalid_target_type')}`)
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !req.user.can(`iam:permission${req.body.target_type}:create`) ) {
|
||||||
|
return res.status(401).api()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure one doesn't already exist
|
||||||
|
const existing = await Permission.findOne({
|
||||||
|
active: true,
|
||||||
|
target_type: req.body.target_type,
|
||||||
|
permission: req.body.permission,
|
||||||
|
})
|
||||||
|
|
||||||
|
if ( existing ) {
|
||||||
|
return res.status(400)
|
||||||
|
.message(req.T('api.permission_already_exists'))
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
const perm = new Permission({
|
||||||
|
target_type: req.body.target_type,
|
||||||
|
permission: req.body.permission,
|
||||||
|
})
|
||||||
|
|
||||||
|
await perm.save()
|
||||||
|
return res.api(await perm.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
async update_policy(req, res, next) {
|
async update_policy(req, res, next) {
|
||||||
const Policy = this.models.get('iam:Policy')
|
const Policy = this.models.get('iam:Policy')
|
||||||
const policy = await Policy.findById(req.params.id)
|
const policy = await Policy.findById(req.params.id)
|
||||||
@ -195,9 +312,9 @@ class IAMController extends Controller {
|
|||||||
.message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`)
|
.message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`)
|
||||||
.api()
|
.api()
|
||||||
|
|
||||||
if ( !['application', 'api_scope'].includes(req.body.target_type) )
|
if ( !['application', 'api_scope', 'machine', 'machine_group'].includes(req.body.target_type) )
|
||||||
return res.status(400)
|
return res.status(400)
|
||||||
.message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope.`)
|
.message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope, machine, machine_group.`)
|
||||||
.api()
|
.api()
|
||||||
|
|
||||||
// Make sure the target_id is valid
|
// Make sure the target_id is valid
|
||||||
@ -214,6 +331,20 @@ class IAMController extends Controller {
|
|||||||
return res.status(400)
|
return res.status(400)
|
||||||
.message(`${req.T('common.invalid')} target_id.`)
|
.message(`${req.T('common.invalid')} target_id.`)
|
||||||
.api()
|
.api()
|
||||||
|
} else if ( req.body.target_type === 'machine' ) {
|
||||||
|
const Machine = this.models.get('ldap:Machine')
|
||||||
|
const machine = await Machine.findById(req.body.target_id)
|
||||||
|
if ( !machine || !machine.active || !req.user.can(`ldap:machine:${machine.id}:view`) )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('common.invalid')} target_id.`)
|
||||||
|
.api()
|
||||||
|
} else if ( req.body.target_type === 'machine_group' ) {
|
||||||
|
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||||
|
const group = await MachineGroup.findById(req.body.target_id)
|
||||||
|
if ( !group || !group.active || !req.user.can(`ldap:machine_group:${group.id}:view`) )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('common.invalid')} target_id.`)
|
||||||
|
.api()
|
||||||
}
|
}
|
||||||
|
|
||||||
policy.entity_type = req.body.entity_type
|
policy.entity_type = req.body.entity_type
|
||||||
@ -221,10 +352,69 @@ class IAMController extends Controller {
|
|||||||
policy.access_type = req.body.access_type
|
policy.access_type = req.body.access_type
|
||||||
policy.target_type = req.body.target_type
|
policy.target_type = req.body.target_type
|
||||||
policy.target_id = req.body.target_id
|
policy.target_id = req.body.target_id
|
||||||
|
|
||||||
|
if ( req.body.permission ) {
|
||||||
|
// Validate the permission and set it, if it is valid
|
||||||
|
const Permission = this.models.get('iam:Permission')
|
||||||
|
const permission = await Permission.findOne({
|
||||||
|
active: true,
|
||||||
|
target_type: req.body.target_type,
|
||||||
|
permission: req.body.permission,
|
||||||
|
})
|
||||||
|
|
||||||
|
if ( permission ) {
|
||||||
|
policy.for_permission = true
|
||||||
|
policy.permission = req.body.permission
|
||||||
|
} else {
|
||||||
|
policy.for_permission = false
|
||||||
|
policy.permission = undefined
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
policy.for_permission = false
|
||||||
|
policy.permission = undefined
|
||||||
|
}
|
||||||
|
|
||||||
await policy.save()
|
await policy.save()
|
||||||
return res.api()
|
return res.api()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update_permission(req, res, next) {
|
||||||
|
const Permission = this.models.get('iam:Permission')
|
||||||
|
|
||||||
|
const required_fields = ['target_type', 'permission']
|
||||||
|
for ( const field of required_fields ) {
|
||||||
|
if ( !req.body[field] )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('api.missing_field')} ${field}`)
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group']
|
||||||
|
if ( !valid_target_types.includes(req.body.target_type) ) {
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('api.invalid_target_type')}`)
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !req.user.can(`iam:permission${req.body.target_type}:update`) ) {
|
||||||
|
return res.status(401).api()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure one doesn't already exist
|
||||||
|
const existing = await Permission.findById(req.params.id)
|
||||||
|
if ( !existing?.active ) {
|
||||||
|
return res.status(404)
|
||||||
|
.message(req.T('api.permission_not_found'))
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.target_type = req.body.target_type
|
||||||
|
existing.permission = req.body.permission
|
||||||
|
|
||||||
|
await existing.save()
|
||||||
|
return res.api(await existing.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
async delete_policy(req, res, next) {
|
async delete_policy(req, res, next) {
|
||||||
const Policy = this.models.get('iam:Policy')
|
const Policy = this.models.get('iam:Policy')
|
||||||
const policy = await Policy.findById(req.params.id)
|
const policy = await Policy.findById(req.params.id)
|
||||||
@ -243,6 +433,27 @@ class IAMController extends Controller {
|
|||||||
await policy.save()
|
await policy.save()
|
||||||
return res.api()
|
return res.api()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delete_permission(req, res, next) {
|
||||||
|
const Permission = this.models.get('iam:Permission')
|
||||||
|
const permission = await Permission.findById(req.params.id)
|
||||||
|
|
||||||
|
if ( !permission?.active ) {
|
||||||
|
return res.status(404)
|
||||||
|
.message(req.T('api.permission_not_found'))
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !req.user.can(`iam:permission:${permission.target_type}:delete`) ) {
|
||||||
|
return res.status(401)
|
||||||
|
.message(req.T('api.insufficient_permissions'))
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
permission.active = false
|
||||||
|
await permission.save()
|
||||||
|
return res.api()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = IAMController
|
module.exports = exports = IAMController
|
||||||
|
@ -46,6 +46,32 @@ class LDAPController extends Controller {
|
|||||||
return res.api(data)
|
return res.api(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get_machines(req, res, next) {
|
||||||
|
const Machine = this.models.get('ldap:Machine')
|
||||||
|
const machines = await Machine.find({active: true})
|
||||||
|
const data = []
|
||||||
|
|
||||||
|
for ( const machine of machines ) {
|
||||||
|
if ( !req.user.can(`ldap:machine:${machine.id}:view`) ) continue
|
||||||
|
data.push(await machine.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.api(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_machine_groups(req, res, next) {
|
||||||
|
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||||
|
const groups = await MachineGroup.find({active: true})
|
||||||
|
const data = []
|
||||||
|
|
||||||
|
for ( const group of groups ) {
|
||||||
|
if ( !req.user.can(`ldap:machine_group:${group.id}:view`) ) continue
|
||||||
|
data.push(await group.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.api(data)
|
||||||
|
}
|
||||||
|
|
||||||
async get_client(req, res, next) {
|
async get_client(req, res, next) {
|
||||||
const Client = this.models.get('ldap:Client')
|
const Client = this.models.get('ldap:Client')
|
||||||
const client = await Client.findById(req.params.id)
|
const client = await Client.findById(req.params.id)
|
||||||
@ -80,6 +106,40 @@ class LDAPController extends Controller {
|
|||||||
return res.api(await group.to_api())
|
return res.api(await group.to_api())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get_machine(req, res, next) {
|
||||||
|
const Machine = this.models.get('ldap:Machine')
|
||||||
|
const machine = await Machine.findById(req.params.id)
|
||||||
|
|
||||||
|
if ( !machine || !machine.active )
|
||||||
|
return res.status(404)
|
||||||
|
.message(req.T('api.machine_not_found'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
if ( !req.user.can(`ldap:machine:${machine.id}:view`) )
|
||||||
|
return res.status(401)
|
||||||
|
.message(req.T('api.insufficient_permissions'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
return res.api(await machine.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_machine_group(req, res, next) {
|
||||||
|
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||||
|
const group = await MachineGroup.findById(req.params.id)
|
||||||
|
|
||||||
|
if ( !group || !group.active )
|
||||||
|
return res.status(404)
|
||||||
|
.message(req.T('api.group_not_found'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
if ( !req.user.can(`ldap:machine_group:${group.id}:view`) )
|
||||||
|
return res.status(401)
|
||||||
|
.message(req.T('api.insufficient_permissions'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
return res.api(await group.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
async create_client(req, res, next) {
|
async create_client(req, res, next) {
|
||||||
if ( !req.user.can('ldap:client:create') )
|
if ( !req.user.can('ldap:client:create') )
|
||||||
return res.status(401)
|
return res.status(401)
|
||||||
@ -121,13 +181,89 @@ class LDAPController extends Controller {
|
|||||||
return res.api(await client.to_api())
|
return res.api(await client.to_api())
|
||||||
}
|
}
|
||||||
|
|
||||||
async create_group(req, res, next) {
|
async create_machine(req, res, next) {
|
||||||
console.log(req.body)
|
// validate inputs
|
||||||
if ( !req.user.can(`ldap:group:create`) )
|
const required_fields = ['name', 'description']
|
||||||
return res.status(401)
|
for ( const field of required_fields ) {
|
||||||
.message(req.T('api.insufficient_permissions'))
|
if ( !req.body[field] )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('api.missing_field')} ${field}`)
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the machine name is free
|
||||||
|
const Machine = this.models.get('ldap:Machine')
|
||||||
|
const existing_machine = await Machine.findOne({ name: req.body.name })
|
||||||
|
if ( existing_machine )
|
||||||
|
return res.status(400)
|
||||||
|
.message(req.T('api.machine_already_exists'))
|
||||||
.api()
|
.api()
|
||||||
|
|
||||||
|
const machine = new Machine({
|
||||||
|
name: req.body.name,
|
||||||
|
description: req.body.description,
|
||||||
|
host_name: req.body.host_name,
|
||||||
|
location: req.body.location,
|
||||||
|
})
|
||||||
|
|
||||||
|
if ( req.body.bind_password ) {
|
||||||
|
await machine.set_bind_password(req.body.bind_password)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'ldap_visible' in req.body ) {
|
||||||
|
machine.ldap_visible = !!req.body.ldap_visible
|
||||||
|
}
|
||||||
|
|
||||||
|
await machine.save()
|
||||||
|
return res.api(await machine.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
|
async create_machine_group(req, res, next) {
|
||||||
|
// validate inputs
|
||||||
|
const required_fields = ['name']
|
||||||
|
for ( const field of required_fields ) {
|
||||||
|
if ( !req.body[field] )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('api.missing_field')} ${field}`)
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the machine name is free
|
||||||
|
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||||
|
const existing_group = await MachineGroup.findOne({ name: req.body.name })
|
||||||
|
if ( existing_group )
|
||||||
|
return res.status(400)
|
||||||
|
.message(req.T('api.group_already_exists'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
const group = new MachineGroup({
|
||||||
|
name: req.body.name,
|
||||||
|
description: req.body.description,
|
||||||
|
})
|
||||||
|
|
||||||
|
if ( 'ldap_visible' in req.body ) {
|
||||||
|
group.ldap_visible = !!req.body.ldap_visible
|
||||||
|
}
|
||||||
|
|
||||||
|
const Machine = this.models.get('ldap:Machine')
|
||||||
|
const machine_ids = Array.isArray(req.body.machine_ids) ? req.body.machine_ids : []
|
||||||
|
group.machine_ids = []
|
||||||
|
for ( const potential of machine_ids ) {
|
||||||
|
const machine = await Machine.findOne({
|
||||||
|
_id: Machine.to_object_id(potential),
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if ( machine ) {
|
||||||
|
group.machine_ids.push(potential)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await group.save()
|
||||||
|
return res.api(await group.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
|
async create_group(req, res, next) {
|
||||||
// validate inputs
|
// validate inputs
|
||||||
const required_fields = ['role', 'name']
|
const required_fields = ['role', 'name']
|
||||||
for ( const field of required_fields ) {
|
for ( const field of required_fields ) {
|
||||||
@ -240,6 +376,106 @@ class LDAPController extends Controller {
|
|||||||
return res.api()
|
return res.api()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update_machine(req, res, next) {
|
||||||
|
const Machine = this.models.get('ldap:Machine')
|
||||||
|
|
||||||
|
const machine = await Machine.findById(req.params.id)
|
||||||
|
if ( !machine || !machine.active )
|
||||||
|
return res.status(404)
|
||||||
|
.message(req.T('api.machine_not_found'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
if ( !req.user.can(`ldap:machine:${machine.id}:update`) )
|
||||||
|
return res.status(401)
|
||||||
|
.message(req.T('api.insufficient_permissions'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
const required_fields = ['name', 'description']
|
||||||
|
for ( const field of required_fields ) {
|
||||||
|
if ( !req.body[field] )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('api.missing_field')} ${field}`)
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the machine name is free
|
||||||
|
const existing_machine = await Machine.findOne({ name: req.body.name })
|
||||||
|
if ( existing_machine && existing_machine.id !== machine.id )
|
||||||
|
return res.status(400)
|
||||||
|
.message(req.T('api.machine_already_exists'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
machine.name = req.body.name
|
||||||
|
machine.description = req.body.description
|
||||||
|
machine.host_name = req.body.host_name
|
||||||
|
machine.location = req.body.location
|
||||||
|
|
||||||
|
if ( req.body.bind_password ) {
|
||||||
|
await machine.set_bind_password(req.body.bind_password)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'ldap_visible' in req.body ) {
|
||||||
|
machine.ldap_visible = !!req.body.ldap_visible
|
||||||
|
}
|
||||||
|
|
||||||
|
await machine.save()
|
||||||
|
return res.api(await machine.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
|
async update_machine_group(req, res, next) {
|
||||||
|
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||||
|
|
||||||
|
const group = await MachineGroup.findById(req.params.id)
|
||||||
|
if ( !group || !group.active )
|
||||||
|
return res.status(404)
|
||||||
|
.message(req.T('api.group_not_found'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
if ( !req.user.can(`ldap:machine_group:${group.id}:update`) )
|
||||||
|
return res.status(401)
|
||||||
|
.message(req.T('api.insufficient_permissions'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
const required_fields = ['name']
|
||||||
|
for ( const field of required_fields ) {
|
||||||
|
if ( !req.body[field] )
|
||||||
|
return res.status(400)
|
||||||
|
.message(`${req.T('api.missing_field')} ${field}`)
|
||||||
|
.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the machine name is free
|
||||||
|
const existing_group = await MachineGroup.findOne({ name: req.body.name })
|
||||||
|
if ( existing_group && existing_group.id !== group.id )
|
||||||
|
return res.status(400)
|
||||||
|
.message(req.T('api.group_already_exists'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
group.name = req.body.name
|
||||||
|
group.description = req.body.description
|
||||||
|
|
||||||
|
if ( 'ldap_visible' in req.body ) {
|
||||||
|
group.ldap_visible = !!req.body.ldap_visible
|
||||||
|
}
|
||||||
|
|
||||||
|
const Machine = this.models.get('ldap:Machine')
|
||||||
|
const machine_ids = Array.isArray(req.body.machine_ids) ? req.body.machine_ids : []
|
||||||
|
group.machine_ids = []
|
||||||
|
for ( const potential of machine_ids ) {
|
||||||
|
const machine = await Machine.findOne({
|
||||||
|
_id: Machine.to_object_id(potential),
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if ( machine ) {
|
||||||
|
group.machine_ids.push(potential)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await group.save()
|
||||||
|
return res.api(await group.to_api())
|
||||||
|
}
|
||||||
|
|
||||||
async update_group(req, res, next) {
|
async update_group(req, res, next) {
|
||||||
const User = await this.models.get('auth:User')
|
const User = await this.models.get('auth:User')
|
||||||
const Group = await this.models.get('ldap:Group')
|
const Group = await this.models.get('ldap:Group')
|
||||||
@ -337,6 +573,44 @@ class LDAPController extends Controller {
|
|||||||
await group.save()
|
await group.save()
|
||||||
return res.api()
|
return res.api()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delete_machine(req, res, next) {
|
||||||
|
const Machine = this.models.get('ldap:Machine')
|
||||||
|
const machine = await Machine.findById(req.params.id)
|
||||||
|
|
||||||
|
if ( !machine || !machine.active )
|
||||||
|
return res.status(404)
|
||||||
|
.message(req.T('api.machine_not_found'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
if ( !req.user.can(`ldap:machine:${machine.id}:delete`) )
|
||||||
|
return res.status(401)
|
||||||
|
.message(req.T('api.insufficient_permissions'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
machine.active = false
|
||||||
|
await machine.save()
|
||||||
|
return res.api()
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete_machine_group(req, res, next) {
|
||||||
|
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||||
|
const group = await MachineGroup.findById(req.params.id)
|
||||||
|
|
||||||
|
if ( !group || !group.active )
|
||||||
|
return res.status(404)
|
||||||
|
.message(req.T('api.group_not_found'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
if ( !req.user.can(`ldap:machine_group:${group.id}:delete`) )
|
||||||
|
return res.status(401)
|
||||||
|
.message(req.T('api.insufficient_permissions'))
|
||||||
|
.api()
|
||||||
|
|
||||||
|
group.active = false
|
||||||
|
await group.save()
|
||||||
|
return res.api()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = LDAPController
|
module.exports = exports = LDAPController
|
||||||
|
@ -20,6 +20,7 @@ class PasswordController extends Controller {
|
|||||||
return {
|
return {
|
||||||
created: x.created,
|
created: x.created,
|
||||||
expires: x.expires,
|
expires: x.expires,
|
||||||
|
accessed: x.accessed,
|
||||||
active: x.active,
|
active: x.active,
|
||||||
name: x.name ?? req.T('common.unnamed'),
|
name: x.name ?? req.T('common.unnamed'),
|
||||||
uuid: x.uuid,
|
uuid: x.uuid,
|
||||||
@ -90,6 +91,10 @@ class PasswordController extends Controller {
|
|||||||
await this.activity.password_reset({ req, ip: req.ip })
|
await this.activity.password_reset({ req, ip: req.ip })
|
||||||
if ( req.trap.has_trap() && req.trap.get_trap() === 'password_reset' ) await req.trap.end()
|
if ( req.trap.has_trap() && req.trap.get_trap() === 'password_reset' ) await req.trap.end()
|
||||||
|
|
||||||
|
if ( req.session.registrant_flow ) {
|
||||||
|
await req.trap.begin('registrant_flow', { session_only: true })
|
||||||
|
}
|
||||||
|
|
||||||
// invalidate existing tokens and other logins
|
// invalidate existing tokens and other logins
|
||||||
await req.user.logout(req)
|
await req.user.logout(req)
|
||||||
await req.user.kickout()
|
await req.user.kickout()
|
||||||
|
@ -24,8 +24,9 @@ class ProfileController extends Controller {
|
|||||||
last_name: user.last_name,
|
last_name: user.last_name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
uid: user.uid,
|
uid: user.uid,
|
||||||
tagline: user.tagline,
|
tagline: user.tagline || '',
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
|
login_shell: user.login_shell || '',
|
||||||
...(user.notify_config ? { notify_config: await user.notify_config.to_api() } : {})
|
...(user.notify_config ? { notify_config: await user.notify_config.to_api() } : {})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -123,6 +124,8 @@ class ProfileController extends Controller {
|
|||||||
|
|
||||||
async update(req, res, next) {
|
async update(req, res, next) {
|
||||||
const User = this.models.get('auth:User')
|
const User = this.models.get('auth:User')
|
||||||
|
const Message = this.models.get('Message')
|
||||||
|
const Setting = this.models.get('Setting')
|
||||||
|
|
||||||
let user
|
let user
|
||||||
if ( req.params.user_id === 'me' ) user = req.user
|
if ( req.params.user_id === 'me' ) user = req.user
|
||||||
@ -154,14 +157,22 @@ class ProfileController extends Controller {
|
|||||||
.api()
|
.api()
|
||||||
|
|
||||||
// Update the user's profile
|
// Update the user's profile
|
||||||
|
if ( user.email !== req.body.email && (await Setting.get('auth.require_email_verify')) ) {
|
||||||
|
await req.trap.begin('verify_email', { session_only: false })
|
||||||
|
await Message.create(req.user, 'Your e-mail address has changed, and a verification e-mail has been sent. You must complete this process to continue.')
|
||||||
|
}
|
||||||
|
|
||||||
user.first_name = req.body.first_name
|
user.first_name = req.body.first_name
|
||||||
user.last_name = req.body.last_name
|
user.last_name = req.body.last_name
|
||||||
user.email = req.body.email
|
user.email = req.body.email
|
||||||
user.tagline = req.body.tagline
|
user.tagline = req.body.tagline
|
||||||
|
user.login_shell = req.body.login_shell
|
||||||
|
|
||||||
// Save the record
|
// Save the record
|
||||||
await user.save()
|
await user.save()
|
||||||
return res.api()
|
return res.api({
|
||||||
|
force_message_refresh: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async update_photo(req, res, next) {
|
async update_photo(req, res, next) {
|
||||||
|
195
app/controllers/api/v1/Radius.controller.js
Normal file
195
app/controllers/api/v1/Radius.controller.js
Normal 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
|
@ -7,15 +7,69 @@ const FormController = require('flitter-auth/controllers/Forms')
|
|||||||
*/
|
*/
|
||||||
class Forms extends FormController {
|
class Forms extends FormController {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'Vue', 'models']
|
return [...super.services, 'Vue', 'models', 'jobs']
|
||||||
}
|
}
|
||||||
|
|
||||||
async registration_provider_get(req, res, next) {
|
async registration_provider_get(req, res, next) {
|
||||||
|
if ( req.session.auth.flow ) {
|
||||||
|
req.session.registrant_flow = req.session.auth.flow
|
||||||
|
}
|
||||||
|
|
||||||
return res.page('auth:register', {
|
return res.page('auth:register', {
|
||||||
...this.Vue.data({})
|
...this.Vue.data({})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async email_verify_keyaction(req, res, next) {
|
||||||
|
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
|
||||||
|
req.user.email_verified = true
|
||||||
|
await req.user.save()
|
||||||
|
await req.trap.end()
|
||||||
|
const url = req.session.email_verify_flow || '/dash/profile'
|
||||||
|
return res.redirect(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
async show_verify_email(req, res, next) {
|
||||||
|
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
|
||||||
|
const verify_queue = this.jobs.queue('verifications')
|
||||||
|
await verify_queue.add('SendVerificationEmail', { user_id: req.user.id })
|
||||||
|
|
||||||
|
return res.page('public:message', {
|
||||||
|
...this.Vue.data({
|
||||||
|
message: req.T('auth.must_verify_email'),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: 'Send Verification E-Mail',
|
||||||
|
action: 'redirect',
|
||||||
|
next: '/auth/verify-email/sent',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async send_verify_email(req, res, next) {
|
||||||
|
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
|
||||||
|
return res.page('public:message', {
|
||||||
|
...this.Vue.data({
|
||||||
|
message: req.T('auth.verify_email_sent'),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: 'Re-send Verification E-Mail',
|
||||||
|
action: 'redirect',
|
||||||
|
next: '/auth/verify-email/sent',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async finish_registration(req, res, next) {
|
||||||
|
if ( req.trap.has_trap() && req.trap.get_trap() === 'registrant_flow' ) await req.trap.end()
|
||||||
|
const dest = req.session.registrant_flow || '/dash/profile'
|
||||||
|
return res.redirect(dest)
|
||||||
|
}
|
||||||
|
|
||||||
async login_provider_get(req, res, next) {
|
async login_provider_get(req, res, next) {
|
||||||
const Setting = this.models.get('Setting')
|
const Setting = this.models.get('Setting')
|
||||||
|
|
||||||
|
@ -23,13 +23,13 @@ class Oauth2 extends Oauth2Controller {
|
|||||||
const Policy = this.models.get('iam:Policy')
|
const Policy = this.models.get('iam:Policy')
|
||||||
const application = await Application.findOne({ oauth_client_ids: starship_client.id })
|
const application = await Application.findOne({ oauth_client_ids: starship_client.id })
|
||||||
if ( !application ) {
|
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, {
|
return this.Vue.auth_message(res, {
|
||||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||||
next_destination: '/dash',
|
next_destination: '/dash',
|
||||||
})
|
})
|
||||||
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
|
} 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, {
|
return this.Vue.auth_message(res, {
|
||||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||||
next_destination: '/dash',
|
next_destination: '/dash',
|
||||||
@ -44,7 +44,7 @@ class Oauth2 extends Oauth2Controller {
|
|||||||
async authorize_get(req, res, next) {
|
async authorize_get(req, res, next) {
|
||||||
const client = await this._get_authorize_client(req)
|
const client = await this._get_authorize_client(req)
|
||||||
if ( !client ) return this._uniform(res, req.T('auth.unable_to_authorize'))
|
if ( !client ) return this._uniform(res, req.T('auth.unable_to_authorize'))
|
||||||
const uri = new URL(req.query.redirect_uri)
|
const uri = new URL(Array.isArray(req.query.redirect_uri) ? req.query.redirect_uri[0] : req.query.redirect_uri)
|
||||||
|
|
||||||
const StarshipClient = this.models.get('oauth:Client')
|
const StarshipClient = this.models.get('oauth:Client')
|
||||||
const starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID })
|
const starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID })
|
||||||
@ -54,19 +54,25 @@ class Oauth2 extends Oauth2Controller {
|
|||||||
const Policy = this.models.get('iam:Policy')
|
const Policy = this.models.get('iam:Policy')
|
||||||
const application = await Application.findOne({ oauth_client_ids: starship_client.id })
|
const application = await Application.findOne({ oauth_client_ids: starship_client.id })
|
||||||
if ( !application ) {
|
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, {
|
return this.Vue.auth_message(res, {
|
||||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||||
next_destination: '/dash',
|
next_destination: '/dash',
|
||||||
})
|
})
|
||||||
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
|
} 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, {
|
return this.Vue.auth_message(res, {
|
||||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||||
next_destination: '/dash',
|
next_destination: '/dash',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let state;
|
||||||
|
if ( state = (req.query.state || req.body.state) ) {
|
||||||
|
state = Array.isArray(state) ? state[0] : state
|
||||||
|
uri.searchParams.set('state', state)
|
||||||
|
}
|
||||||
|
|
||||||
if ( req.user.has_authorized(starship_client) ) {
|
if ( req.user.has_authorized(starship_client) ) {
|
||||||
return this.Vue.invoke_action(res, {
|
return this.Vue.invoke_action(res, {
|
||||||
text: 'Grant Access',
|
text: 'Grant Access',
|
||||||
|
@ -12,7 +12,7 @@ class EMailJob extends Job {
|
|||||||
|
|
||||||
const { data } = job
|
const { data } = job
|
||||||
let { from = config.default_sender, to, subject, html = undefined, email_params = undefined } = data
|
let { from = config.default_sender, to, subject, html = undefined, email_params = undefined } = data
|
||||||
this.output.info(`Sending mail to ${to}...`)
|
this.info(`Sending mail to ${to}...`)
|
||||||
|
|
||||||
if ( !html && email_params ) html = this.email(email_params)
|
if ( !html && email_params ) html = this.email(email_params)
|
||||||
|
|
||||||
@ -20,9 +20,11 @@ class EMailJob extends Job {
|
|||||||
from, to, subject, html,
|
from, to, subject, html,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.output.error(e)
|
this.error(e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
this.output.success(`Mail sent!`)
|
|
||||||
|
this.success(`Mail sent!`)
|
||||||
}
|
}
|
||||||
|
|
||||||
email({ header_text, body_paragraphs = [], button_text = '', button_link = '' }) {
|
email({ header_text, body_paragraphs = [], button_text = '', button_link = '' }) {
|
||||||
|
@ -12,7 +12,7 @@ class ForeignIPLoginAlertJob extends Job {
|
|||||||
const user = await User.findById(user_id)
|
const user = await User.findById(user_id)
|
||||||
if ( !user ) throw new Error('Unable to find user with ID: '+user_id)
|
if ( !user ) throw new Error('Unable to find user with ID: '+user_id)
|
||||||
|
|
||||||
this.output.info('Sending foreign IP login alert to user.')
|
this.info('Sending foreign IP login alert to user ' + user.uid)
|
||||||
|
|
||||||
await this.jobs.queue('mailer').add('EMail', {
|
await this.jobs.queue('mailer').add('EMail', {
|
||||||
to: user.email,
|
to: user.email,
|
||||||
@ -29,14 +29,19 @@ class ForeignIPLoginAlertJob extends Job {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.info('Logged e-mail job')
|
||||||
|
|
||||||
if ( user.notify_config && user.notify_config.active ) {
|
if ( user.notify_config && user.notify_config.active ) {
|
||||||
await user.notify_config.log({
|
await user.notify_config.log({
|
||||||
title: `${this.configs.get('app.name')}: Sign-In From New IP`,
|
title: `${this.configs.get('app.name')}: Sign-In From New IP`,
|
||||||
message: `Someone signed into your account (${user.uid}) from the IP address ${ip}. If this was you, no further action is required.`,
|
message: `Someone signed into your account (${user.uid}) from the IP address ${ip}. If this was you, no further action is required.`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.info('Logged push notification job')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.output.error(e)
|
this.error(e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,13 +13,17 @@ class PasswordResetJob extends Job {
|
|||||||
const User = this.models.get('auth:User')
|
const User = this.models.get('auth:User')
|
||||||
const user = await User.findById(user_id)
|
const user = await User.findById(user_id)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
this.output.error(`Unable to find user with ID: ${user_id}`)
|
this.error(`Unable to find user with ID: ${user_id}`)
|
||||||
throw new Error('Unable to find user with that ID.')
|
throw new Error('Unable to find user with that ID.')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.output.info(`Resetting password for user: ${user.uid}`)
|
this.info(`Resetting password for user: ${user.uid}`)
|
||||||
|
|
||||||
// Create an authenticated key-action
|
// Create an authenticated key-action
|
||||||
const key_action = await this.key_action(user)
|
const key_action = await this.key_action(user)
|
||||||
|
|
||||||
|
this.info(`Created reset keyaction ${key_action.id} (key: ${key_action.key}, handler: ${key_action.handler})`)
|
||||||
|
|
||||||
await this.jobs.queue('mailer').add('EMail', {
|
await this.jobs.queue('mailer').add('EMail', {
|
||||||
to: user.email,
|
to: user.email,
|
||||||
subject: 'Reset Your Password | ' + this.configs.get('app.name'),
|
subject: 'Reset Your Password | ' + this.configs.get('app.name'),
|
||||||
@ -34,17 +38,22 @@ class PasswordResetJob extends Job {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.info('Logged e-mail job.')
|
||||||
|
|
||||||
if ( user.notify_config && user.notify_config.active ) {
|
if ( user.notify_config && user.notify_config.active ) {
|
||||||
await user.notify_config.log({
|
await user.notify_config.log({
|
||||||
title: `${this.configs.get('app.name')}: Password Reset Requested`,
|
title: `${this.configs.get('app.name')}: Password Reset Requested`,
|
||||||
message: `A password reset request was logged for your account (${user.uid}). If this was you, please check your e-mail for further instructions.`,
|
message: `A password reset request was logged for your account (${user.uid}). If this was you, please check your e-mail for further instructions.`,
|
||||||
priority: 8,
|
priority: 8,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.info('Logged security push notification job')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.output.success('Password reset logged.')
|
this.success('Password reset logged.')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.output.error(e)
|
this.error(e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ class PasswordResetAlertJob extends Job {
|
|||||||
const user = await User.findById(user_id)
|
const user = await User.findById(user_id)
|
||||||
if ( !user ) throw new Error('Unable to find user with ID: '+user_id)
|
if ( !user ) throw new Error('Unable to find user with ID: '+user_id)
|
||||||
|
|
||||||
this.output.info('Sending password reset alert to user.')
|
this.info('Sending password reset alert to user ' + user.uid)
|
||||||
|
|
||||||
await this.jobs.queue('mailer').add('EMail', {
|
await this.jobs.queue('mailer').add('EMail', {
|
||||||
to: user.email,
|
to: user.email,
|
||||||
@ -28,15 +28,20 @@ class PasswordResetAlertJob extends Job {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.info('Logged e-mail job')
|
||||||
|
|
||||||
if ( user.notify_config && user.notify_config.active ) {
|
if ( user.notify_config && user.notify_config.active ) {
|
||||||
await user.notify_config.log({
|
await user.notify_config.log({
|
||||||
title: `${this.configs.get('app.name')}: Password Reset`,
|
title: `${this.configs.get('app.name')}: Password Reset`,
|
||||||
message: `The password to your account (${user.uid}) was reset from the IP address ${ip}. If this was not you, please contact your system administrator.`,
|
message: `The password to your account (${user.uid}) was reset from the IP address ${ip}. If this was not you, please contact your system administrator.`,
|
||||||
priority: 8,
|
priority: 8,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.info('Logged push notification job')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.output.error(e)
|
this.error(e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,14 +14,15 @@ class PopulateAnnouncementJob extends Job {
|
|||||||
const announcement = await Announcement.findById(announcement_id)
|
const announcement = await Announcement.findById(announcement_id)
|
||||||
|
|
||||||
if ( !announcement ) {
|
if ( !announcement ) {
|
||||||
this.output.error(`Unable to find announcement with ID: ${announcement_id}`)
|
this.error(`Unable to find announcement with ID: ${announcement_id}`)
|
||||||
throw new Error('Unable to find announcement with that ID.')
|
throw new Error('Unable to find announcement with that ID.')
|
||||||
}
|
}
|
||||||
|
|
||||||
await announcement.populate()
|
await announcement.populate()
|
||||||
this.output.success('Populated announcements.')
|
this.success('Populated announcements.')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.output.error(e)
|
this.error(e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,13 +18,15 @@ class PushNotifyJob extends Job {
|
|||||||
const notify = user.notify_config
|
const notify = user.notify_config
|
||||||
if ( !notify || !notify.active ) throw new Error('User does not have notifications configured.')
|
if ( !notify || !notify.active ) throw new Error('User does not have notifications configured.')
|
||||||
|
|
||||||
this.output.info(`Sending notification to ${user.uid}...`)
|
this.info(`Sending notification to ${user.uid}...`)
|
||||||
|
|
||||||
await notify.send({ title, message, priority })
|
await notify.send({ title, message, priority })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.output.error(e)
|
this.error(e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
this.output.success(`Notification sent!`)
|
|
||||||
|
this.success(`Notification sent!`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
62
app/jobs/SendVerificationEmail.job.js
Normal file
62
app/jobs/SendVerificationEmail.job.js
Normal 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
|
@ -1,4 +1,5 @@
|
|||||||
const LDAPController = require('./LDAPController')
|
const LDAPController = require('./LDAPController')
|
||||||
|
const LDAP = require('ldapjs')
|
||||||
|
|
||||||
class GroupsController extends LDAPController {
|
class GroupsController extends LDAPController {
|
||||||
static get services() {
|
static get services() {
|
||||||
|
@ -59,34 +59,8 @@ class LDAPController extends Injectable {
|
|||||||
return next(new LDAP.InsufficientAccessRightsError())
|
return next(new LDAP.InsufficientAccessRightsError())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the credentials are an app_password
|
// Check if the credentials are valid
|
||||||
const app_password_verified = Array.isArray(item.app_passwords)
|
if ( !(await item.check_credential_string(req.credentials)) ) {
|
||||||
&& item.app_passwords.length > 0
|
|
||||||
&& await item.check_app_password(req.credentials)
|
|
||||||
|
|
||||||
// Check if the user has MFA enabled.
|
|
||||||
// If so, split the incoming password to fetch the MFA code
|
|
||||||
// e.g. normalPassword:123456
|
|
||||||
if ( !app_password_verified && item.mfa_enabled ) {
|
|
||||||
const parts = req.credentials.split(':')
|
|
||||||
const mfa_code = parts.pop()
|
|
||||||
const actual_password = parts.join(':')
|
|
||||||
|
|
||||||
// Check the credentials
|
|
||||||
if ( !await item.check_password(actual_password) ) {
|
|
||||||
this.output.debug(`Bind failure: user w/ MFA provided invalid credentials`)
|
|
||||||
return next(new LDAP.InvalidCredentialsError('Invalid credentials. Make sure MFA code is included at the end of your password (e.g. password:123456)'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, check the MFA code
|
|
||||||
if ( !item.mfa_token.verify(mfa_code) ) {
|
|
||||||
this.output.debug(`Bind failure: user w/ MFA provided invalid MFA token`)
|
|
||||||
return next(new LDAP.InvalidCredentialsError('Invalid credentials. Verification of the MFA token failed.'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not MFA, just check the credentials
|
|
||||||
} else if (!app_password_verified && !await item.check_password(req.credentials)) {
|
|
||||||
this.output.debug(`Bind failure: user w/ simple auth provided invalid credentials`)
|
|
||||||
return next(new LDAP.InvalidCredentialsError())
|
return next(new LDAP.InvalidCredentialsError())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
146
app/ldap/controllers/Sudo.controller.js
Normal file
146
app/ldap/controllers/Sudo.controller.js
Normal 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
|
28
app/ldap/routes/sudo.routes.js
Normal file
28
app/ldap/routes/sudo.routes.js
Normal 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
|
@ -11,6 +11,7 @@ class ApplicationModel extends Model {
|
|||||||
ldap_client_ids: [String],
|
ldap_client_ids: [String],
|
||||||
oauth_client_ids: [String],
|
oauth_client_ids: [String],
|
||||||
openid_client_ids: [String],
|
openid_client_ids: [String],
|
||||||
|
radius_client_ids: [String],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ class ApplicationModel extends Model {
|
|||||||
ldap_client_ids: this.ldap_client_ids,
|
ldap_client_ids: this.ldap_client_ids,
|
||||||
oauth_client_ids: this.oauth_client_ids,
|
oauth_client_ids: this.oauth_client_ids,
|
||||||
openid_client_ids: this.openid_client_ids,
|
openid_client_ids: this.openid_client_ids,
|
||||||
|
radius_client_ids: this.radius_client_ids || [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ class AppPasswordModel extends Model {
|
|||||||
return {
|
return {
|
||||||
hash: String,
|
hash: String,
|
||||||
created: { type: Date, default: () => new Date },
|
created: { type: Date, default: () => new Date },
|
||||||
|
accessed: Date,
|
||||||
expires: Date,
|
expires: Date,
|
||||||
active: { type: Boolean, default: true },
|
active: { type: Boolean, default: true },
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -11,6 +11,9 @@ class GroupModel extends Model {
|
|||||||
return {
|
return {
|
||||||
name: String,
|
name: String,
|
||||||
user_ids: [String],
|
user_ids: [String],
|
||||||
|
posix_user_id: String,
|
||||||
|
posix_group_id: Number,
|
||||||
|
grants_sudo: { type: Boolean, default: false },
|
||||||
active: { type: Boolean, default: true },
|
active: { type: Boolean, default: true },
|
||||||
ldap_visible: { type: Boolean, default: true },
|
ldap_visible: { type: Boolean, default: true },
|
||||||
}
|
}
|
||||||
@ -29,18 +32,72 @@ class GroupModel extends Model {
|
|||||||
return await User.find({ _id: { $in: this.user_ids.map(x => this.constructor.to_object_id(x)) } })
|
return await User.find({ _id: { $in: this.user_ids.map(x => this.constructor.to_object_id(x)) } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get_gid_number() {
|
||||||
|
if ( !this.posix_group_id ) {
|
||||||
|
const Setting = this.models.get('Setting')
|
||||||
|
let last_uid = await Setting.get('ldap.last_alloc_uid')
|
||||||
|
if ( last_uid < 1 ) {
|
||||||
|
last_uid = this.configs.get('ldap:server.schema.start_uid')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.posix_group_id = last_uid + 1
|
||||||
|
await Setting.set('ldap.last_alloc_uid', this.posix_group_id)
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.posix_group_id
|
||||||
|
}
|
||||||
|
|
||||||
async to_ldap() {
|
async to_ldap() {
|
||||||
const users = await this.users()
|
const users = await this.users()
|
||||||
return {
|
return {
|
||||||
cn: this.name,
|
cn: this.name,
|
||||||
dn: this.dn.format(this.configs.get('ldap:server.format')),
|
dn: this.dn.format(this.configs.get('ldap:server.format')),
|
||||||
objectClass: 'groupOfNames',
|
objectClass: ['groupOfNames', 'posixGroup'],
|
||||||
|
gidNumber: String(await this.get_gid_number()),
|
||||||
member: users.map(x => x.dn.format(this.configs.get('ldap:server.format'))),
|
member: users.map(x => x.dn.format(this.configs.get('ldap:server.format'))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async sudo_directory() {
|
||||||
|
const groups = await this.find({ ldap_visible: true, active: true, grants_sudo: true })
|
||||||
|
|
||||||
|
let users = []
|
||||||
|
for ( const group of groups ) {
|
||||||
|
users = [...users, ...(await group.users())]
|
||||||
|
}
|
||||||
|
|
||||||
|
return users.filter(u => u.uid !== 'root')
|
||||||
|
}
|
||||||
|
|
||||||
static async ldap_directory() {
|
static async ldap_directory() {
|
||||||
return this.find({ ldap_visible: true, active: true })
|
const User = this.prototype.models.get('auth:User')
|
||||||
|
const groups = await this.find({ ldap_visible: true, active: true })
|
||||||
|
|
||||||
|
const posix_user_ids = groups.map(group => group.posix_user_id)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(id => User.to_object_id(id))
|
||||||
|
|
||||||
|
const missing_posix_users = await User.find({
|
||||||
|
ldap_visible: true,
|
||||||
|
_id: {
|
||||||
|
$nin: posix_user_ids
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for ( const user of missing_posix_users ) {
|
||||||
|
const group = new this({
|
||||||
|
name: `${user.uid} (posix)`,
|
||||||
|
user_ids: [user.id],
|
||||||
|
posix_user_id: user.id,
|
||||||
|
posix_group_id: await user.get_uid_number(),
|
||||||
|
})
|
||||||
|
|
||||||
|
await group.save()
|
||||||
|
groups.push(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
async to_api() {
|
async to_api() {
|
||||||
@ -49,6 +106,7 @@ class GroupModel extends Model {
|
|||||||
name: this.name,
|
name: this.name,
|
||||||
user_ids: this.user_ids,
|
user_ids: this.user_ids,
|
||||||
ldap_visible: this.ldap_visible,
|
ldap_visible: this.ldap_visible,
|
||||||
|
grants_sudo: !!this.grants_sudo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ class User extends AuthUser {
|
|||||||
last_name: String,
|
last_name: String,
|
||||||
tagline: String,
|
tagline: String,
|
||||||
email: String,
|
email: String,
|
||||||
|
email_verified: {type: Boolean, default: false},
|
||||||
ldap_visible: {type: Boolean, default: true},
|
ldap_visible: {type: Boolean, default: true},
|
||||||
active: {type: Boolean, default: true},
|
active: {type: Boolean, default: true},
|
||||||
mfa_token: MFAToken,
|
mfa_token: MFAToken,
|
||||||
@ -38,9 +39,42 @@ class User extends AuthUser {
|
|||||||
photo_file_id: String,
|
photo_file_id: String,
|
||||||
trap: String,
|
trap: String,
|
||||||
notify_config: NotifyConfig,
|
notify_config: NotifyConfig,
|
||||||
|
uid_number: Number,
|
||||||
|
login_shell: String,
|
||||||
|
|
||||||
|
is_default_user_for_coreid: { type: Boolean, default: false },
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async grant_defaults() {
|
||||||
|
const default_user = await this.constructor.findOne({is_default_user_for_coreid: true, active: true})
|
||||||
|
this.login_shell = default_user.login_shell
|
||||||
|
this.roles = default_user.roles
|
||||||
|
this.permissions = default_user.permissions
|
||||||
|
|
||||||
|
const groups = await default_user.groups()
|
||||||
|
for ( const group of groups ) {
|
||||||
|
group.user_ids.push(this.id)
|
||||||
|
await group.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_uid_number() {
|
||||||
|
if ( !this.uid_number ) {
|
||||||
|
const Setting = this.models.get('Setting')
|
||||||
|
let last_uid = await Setting.get('ldap.last_alloc_uid')
|
||||||
|
if ( last_uid < 1 ) {
|
||||||
|
last_uid = this.configs.get('ldap:server.schema.start_uid')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uid_number = last_uid + 1
|
||||||
|
await Setting.set('ldap.last_alloc_uid', this.uid_number)
|
||||||
|
await this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.uid_number
|
||||||
|
}
|
||||||
|
|
||||||
async photo() {
|
async photo() {
|
||||||
const File = this.models.get('upload::File')
|
const File = this.models.get('upload::File')
|
||||||
return File.findById(this.photo_file_id)
|
return File.findById(this.photo_file_id)
|
||||||
@ -77,10 +111,12 @@ class User extends AuthUser {
|
|||||||
uid: this.uid,
|
uid: this.uid,
|
||||||
first_name: this.first_name,
|
first_name: this.first_name,
|
||||||
last_name: this.last_name,
|
last_name: this.last_name,
|
||||||
|
name: `${this.first_name} ${this.last_name}`,
|
||||||
email: this.email,
|
email: this.email,
|
||||||
tagline: this.tagline,
|
tagline: this.tagline,
|
||||||
trap: this.trap,
|
trap: this.trap,
|
||||||
group_ids: (await this.groups()).map(x => x.id),
|
group_ids: (await this.groups()).map(x => x.id),
|
||||||
|
profile_photo: `${this.configs.get('app.url')}api/v1/auth/users/${this.uid}/photo`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,13 +155,49 @@ class User extends AuthUser {
|
|||||||
await this.save()
|
await this.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async check_credential_string(credential) {
|
||||||
|
// Check if the credentials are an app_password
|
||||||
|
const app_password_verified = Array.isArray(this.app_passwords)
|
||||||
|
&& this.app_passwords.length > 0
|
||||||
|
&& await this.check_app_password(credential)
|
||||||
|
|
||||||
|
// Check if the user has MFA enabled.
|
||||||
|
// If so, split the incoming password to fetch the MFA code
|
||||||
|
// e.g. normalPassword:123456
|
||||||
|
if ( !app_password_verified && this.mfa_enabled ) {
|
||||||
|
const parts = credential.split(':')
|
||||||
|
const mfa_code = parts.pop()
|
||||||
|
const actual_password = parts.join(':')
|
||||||
|
|
||||||
|
// Check the credentials
|
||||||
|
if ( !await this.check_password(actual_password) ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, check the MFA code
|
||||||
|
if ( !this.mfa_token.verify(mfa_code) ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not MFA, just check the credentials
|
||||||
|
} else if (!app_password_verified && !await this.check_password(credential)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
async check_password(password) {
|
async check_password(password) {
|
||||||
return this.get_provider().check_user_auth(this, password)
|
return this.get_provider().check_user_auth(this, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
async check_app_password(password) {
|
async check_app_password(password) {
|
||||||
for ( const pw of this.app_passwords ) {
|
for ( const pw of this.app_passwords ) {
|
||||||
if ( await pw.verify(password) ) return true
|
if ( await pw.verify(password) ) {
|
||||||
|
pw.accessed = new Date
|
||||||
|
await pw.save()
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@ -169,9 +241,45 @@ class User extends AuthUser {
|
|||||||
this.get_provider().logout(request)
|
this.get_provider().logout(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async has_sudo() {
|
||||||
|
const groups = await this.groups()
|
||||||
|
return groups.some(group => group.grants_sudo)
|
||||||
|
}
|
||||||
|
|
||||||
|
async to_sudo(iam_targets = []) {
|
||||||
|
const Policy = this.models.get('iam:Policy')
|
||||||
|
const granted = []
|
||||||
|
for ( const target of iam_targets ) {
|
||||||
|
if ( await Policy.check_user_access(this, target, 'sudo') ) {
|
||||||
|
granted.push(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
objectClass: ['sudoRole'],
|
||||||
|
cn: `sudo_${this.uid.toLowerCase()}`,
|
||||||
|
sudoUser: this.uid.toLowerCase(),
|
||||||
|
...(granted.length ? {
|
||||||
|
iamtarget: granted,
|
||||||
|
sudoHost: 'ALL',
|
||||||
|
sudoRunAs: 'ALL',
|
||||||
|
sudoCommand: 'ALL',
|
||||||
|
} : {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async to_ldap(iam_targets = []) {
|
async to_ldap(iam_targets = []) {
|
||||||
const Policy = this.models.get('iam:Policy')
|
const Policy = this.models.get('iam:Policy')
|
||||||
|
|
||||||
|
const uid_number = await this.get_uid_number()
|
||||||
|
const shell = this.login_shell || this.configs.get('ldap:server.schema.default_shell')
|
||||||
|
const domain = this.configs.get('ldap:server.schema.base_dc').split(',').map(x => x.replace('dc=', '')).join('.')
|
||||||
|
|
||||||
|
const group_ids = []
|
||||||
|
for ( const group of await this.groups() ) {
|
||||||
|
group_ids.push(await group.get_gid_number())
|
||||||
|
}
|
||||||
|
|
||||||
const ldap_data = {
|
const ldap_data = {
|
||||||
uid: this.uid.toLowerCase(),
|
uid: this.uid.toLowerCase(),
|
||||||
uuid: this.uuid,
|
uuid: this.uuid,
|
||||||
@ -179,10 +287,16 @@ class User extends AuthUser {
|
|||||||
sn: this.last_name,
|
sn: this.last_name,
|
||||||
gecos: `${this.first_name} ${this.last_name}`,
|
gecos: `${this.first_name} ${this.last_name}`,
|
||||||
mail: this.email,
|
mail: this.email,
|
||||||
objectClass: ['inetOrgPerson', 'person'],
|
objectClass: ['inetOrgPerson', 'person', 'posixaccount'],
|
||||||
|
objectclass: ['inetOrgPerson', 'person', 'posixaccount'],
|
||||||
entryuuid: this.uuid,
|
entryuuid: this.uuid,
|
||||||
entryUUID: this.uuid,
|
entryUUID: this.uuid,
|
||||||
objectGuid: this.uuid,
|
objectGuid: this.uuid,
|
||||||
|
objectguid: this.uuid,
|
||||||
|
uidNumber: uid_number,
|
||||||
|
gidNumber: String(await this.get_uid_number()), // group_ids.map(x => String(x)),
|
||||||
|
loginShell: shell,
|
||||||
|
homeDirectory: `/home/${this.uid}@${domain}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( this.tagline ) ldap_data.extras_tagline = this.tagline
|
if ( this.tagline ) ldap_data.extras_tagline = this.tagline
|
||||||
@ -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'))}`)
|
return LDAP.parseDN(`uid=${this.uid.toLowerCase()},${this.ldap_server.auth_dn().format(this.configs.get('ldap:server.format'))}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get sudo_dn() {
|
||||||
|
return LDAP.parseDN(`cn=sudo_${this.uid.toLowerCase()},${this.ldap_server.sudo_dn().format(this.configs.get('ldap:server.format'))}`)
|
||||||
|
}
|
||||||
|
|
||||||
// The following are used by OpenID connect
|
// The following are used by OpenID connect
|
||||||
|
|
||||||
async claims(use, scope) {
|
async claims(use, scope) {
|
||||||
|
23
app/models/iam/Permission.model.js
Normal file
23
app/models/iam/Permission.model.js
Normal 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
|
@ -12,39 +12,49 @@ class PolicyModel extends Model {
|
|||||||
entity_type: String, // user | group
|
entity_type: String, // user | group
|
||||||
entity_id: String,
|
entity_id: String,
|
||||||
access_type: String, // allow | deny
|
access_type: String, // allow | deny
|
||||||
target_type: { type: String, default: 'application' }, // application | api_scope
|
target_type: { type: String, default: 'application' }, // application | api_scope | machine | machine_group
|
||||||
target_id: String,
|
target_id: String,
|
||||||
active: { type: Boolean, default: true },
|
active: { type: Boolean, default: true },
|
||||||
|
for_permission: { type: Boolean, default: false },
|
||||||
|
permission: String,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async check_allow(entity_id, target_id) {
|
static async check_allow(entity_id, target_id, permission = undefined) {
|
||||||
const policies = await this.find({
|
const policies = await this.find({
|
||||||
entity_id,
|
entity_id,
|
||||||
target_id,
|
target_id,
|
||||||
access_type: 'allow',
|
access_type: 'allow',
|
||||||
active: true,
|
active: true,
|
||||||
|
...(permission ? {
|
||||||
|
for_permission: true,
|
||||||
|
permission,
|
||||||
|
} : {})
|
||||||
})
|
})
|
||||||
|
|
||||||
return policies.length > 0
|
return policies.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
static async check_deny(entity_id, target_id) {
|
static async check_deny(entity_id, target_id, permission = undefined) {
|
||||||
const policies = await this.find({
|
const policies = await this.find({
|
||||||
entity_id,
|
entity_id,
|
||||||
target_id,
|
target_id,
|
||||||
access_type: 'deny',
|
access_type: 'deny',
|
||||||
active: true,
|
active: true,
|
||||||
|
...(permission ? {
|
||||||
|
for_permission: true,
|
||||||
|
permission,
|
||||||
|
} : {})
|
||||||
})
|
})
|
||||||
|
|
||||||
return policies.length === 0
|
return policies.length === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
static async check_entity_access(entity_id, target_id) {
|
static async check_entity_access(entity_id, target_id, permission = undefined) {
|
||||||
return (await this.check_allow(entity_id, target_id)) && !(await this.check_deny(entity_id, target_id))
|
return (await this.check_allow(entity_id, target_id, permission)) && !(await this.check_deny(entity_id, target_id, permission))
|
||||||
}
|
}
|
||||||
|
|
||||||
static async check_user_denied(user, target_id) {
|
static async check_user_denied(user, target_id, permission = undefined) {
|
||||||
const groups = await user.groups()
|
const groups = await user.groups()
|
||||||
const group_ids = groups.map(x => x.id)
|
const group_ids = groups.map(x => x.id)
|
||||||
|
|
||||||
@ -53,6 +63,10 @@ class PolicyModel extends Model {
|
|||||||
target_id,
|
target_id,
|
||||||
access_type: 'deny',
|
access_type: 'deny',
|
||||||
active: true,
|
active: true,
|
||||||
|
...(permission ? {
|
||||||
|
for_permission: true,
|
||||||
|
permission,
|
||||||
|
} : {})
|
||||||
})
|
})
|
||||||
|
|
||||||
const group_denials = await this.find({
|
const group_denials = await this.find({
|
||||||
@ -60,41 +74,92 @@ class PolicyModel extends Model {
|
|||||||
target_id,
|
target_id,
|
||||||
access_type: 'deny',
|
access_type: 'deny',
|
||||||
active: true,
|
active: true,
|
||||||
|
...(permission ? {
|
||||||
|
for_permission: true,
|
||||||
|
permission,
|
||||||
|
} : {})
|
||||||
})
|
})
|
||||||
|
|
||||||
return user_denials.length > 0 || group_denials.length > 0
|
return user_denials.length > 0 || group_denials.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
static async check_user_access(user, target_id) {
|
static async get_all_related(target_id) {
|
||||||
|
const all = [target_id]
|
||||||
|
const Machine = this.prototype.models.get('ldap:Machine')
|
||||||
|
const MachineGroup = this.prototype.models.get('ldap:MachineGroup')
|
||||||
|
|
||||||
|
const machine = await Machine.findById(target_id)
|
||||||
|
if ( machine?.active ) {
|
||||||
|
const groups = await MachineGroup.find({
|
||||||
|
active: true,
|
||||||
|
machine_ids: machine.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
groups.map(x => all.push(x.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = await MachineGroup.findById(target_id)
|
||||||
|
if ( group?.active ) {
|
||||||
|
const machines = await Machine.find({
|
||||||
|
active: true,
|
||||||
|
_id: {
|
||||||
|
$in: group.machine_ids.map(x => Machine.to_object_id(x)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
machines.map(x => all.push(x.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
static async check_user_access(user, target_id, permission = undefined) {
|
||||||
const groups = await user.groups()
|
const groups = await user.groups()
|
||||||
const group_ids = groups.map(x => x.id)
|
const group_ids = groups.map(x => x.id)
|
||||||
|
const target_ids = await this.get_all_related(target_id)
|
||||||
|
|
||||||
const user_approvals = await this.find({
|
const user_approvals = await this.find({
|
||||||
entity_id: user.id,
|
entity_id: user.id,
|
||||||
target_id,
|
target_id: { $in: target_ids },
|
||||||
access_type: 'allow',
|
access_type: 'allow',
|
||||||
active: true,
|
active: true,
|
||||||
|
...(permission ? {
|
||||||
|
for_permission: true,
|
||||||
|
permission,
|
||||||
|
} : {})
|
||||||
})
|
})
|
||||||
|
|
||||||
const user_denials = await this.find({
|
const user_denials = await this.find({
|
||||||
entity_id: user.id,
|
entity_id: user.id,
|
||||||
target_id,
|
target_id: { $in: target_ids },
|
||||||
access_type: 'deny',
|
access_type: 'deny',
|
||||||
active: true,
|
active: true,
|
||||||
|
...(permission ? {
|
||||||
|
for_permission: true,
|
||||||
|
permission,
|
||||||
|
} : {})
|
||||||
})
|
})
|
||||||
|
|
||||||
const group_approvals = await this.find({
|
const group_approvals = await this.find({
|
||||||
entity_id: { $in: group_ids },
|
entity_id: { $in: group_ids },
|
||||||
target_id,
|
target_id: { $in: target_ids },
|
||||||
access_type: 'allow',
|
access_type: 'allow',
|
||||||
active: true,
|
active: true,
|
||||||
|
...(permission ? {
|
||||||
|
for_permission: true,
|
||||||
|
permission,
|
||||||
|
} : {})
|
||||||
})
|
})
|
||||||
|
|
||||||
const group_denials = await this.find({
|
const group_denials = await this.find({
|
||||||
entity_id: { $in: group_ids },
|
entity_id: { $in: group_ids },
|
||||||
target_id,
|
target_id: { $in: target_ids },
|
||||||
access_type: 'deny',
|
access_type: 'deny',
|
||||||
active: true,
|
active: true,
|
||||||
|
...(permission ? {
|
||||||
|
for_permission: true,
|
||||||
|
permission,
|
||||||
|
} : {})
|
||||||
})
|
})
|
||||||
|
|
||||||
// IF user has explicit denial, deny
|
// IF user has explicit denial, deny
|
||||||
@ -132,6 +197,18 @@ class PolicyModel extends Model {
|
|||||||
target_display = `Application: ${app.name}`
|
target_display = `Application: ${app.name}`
|
||||||
} else if ( this.target_type === 'api_scope' ) {
|
} else if ( this.target_type === 'api_scope' ) {
|
||||||
target_display = `API Scope: ${this.target_id}`
|
target_display = `API Scope: ${this.target_id}`
|
||||||
|
} else if ( this.target_type === 'machine' ) {
|
||||||
|
const Machine = this.models.get('ldap:Machine')
|
||||||
|
const machine = await Machine.findById(this.target_id)
|
||||||
|
target_display = `Computer: ${machine.name}`
|
||||||
|
|
||||||
|
if ( machine.host_name ) {
|
||||||
|
target_display += ` (${machine.host_name})`
|
||||||
|
}
|
||||||
|
} else if ( this.target_type === 'machine_group' ) {
|
||||||
|
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||||
|
const group = await MachineGroup.findById(this.target_id)
|
||||||
|
target_display = `Computer Group: ${group.name} (${group.machine_ids.length} computers)`
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -143,6 +220,8 @@ class PolicyModel extends Model {
|
|||||||
target_display,
|
target_display,
|
||||||
target_type: this.target_type,
|
target_type: this.target_type,
|
||||||
target_id: this.target_id,
|
target_id: this.target_id,
|
||||||
|
for_permission: this.for_permission,
|
||||||
|
permission: this.permission,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
73
app/models/ldap/Machine.model.js
Normal file
73
app/models/ldap/Machine.model.js
Normal 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
|
47
app/models/ldap/MachineGroup.model.js
Normal file
47
app/models/ldap/MachineGroup.model.js
Normal 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
|
32
app/models/radius/Client.model.js
Normal file
32
app/models/radius/Client.model.js
Normal 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
|
@ -17,7 +17,9 @@ class SAMLRequestMiddleware extends Middleware {
|
|||||||
// Verify that the issuer is known
|
// Verify that the issuer is known
|
||||||
const sp = await ServiceProvider.findOne({entity_id: data.issuer, active: true})
|
const sp = await ServiceProvider.findOne({entity_id: data.issuer, active: true})
|
||||||
if (!sp)
|
if (!sp)
|
||||||
return res.error(401, 'Unable to continue. The SAML issuer is unknown.')
|
return res.error(401, {
|
||||||
|
message: 'Unable to continue. The SAML issuer is unknown.'
|
||||||
|
})
|
||||||
|
|
||||||
req.saml_request = {
|
req.saml_request = {
|
||||||
relay_state: req.query.RelayState || req.body.RelayState,
|
relay_state: req.query.RelayState || req.body.RelayState,
|
||||||
|
@ -58,7 +58,29 @@ class TrapUtility {
|
|||||||
|
|
||||||
allows(route) {
|
allows(route) {
|
||||||
const config = this.config()
|
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 = {}) {
|
async test(req, res, next, args = {}) {
|
||||||
|
const Setting = this.models.get('Setting')
|
||||||
req.trap = new TrapUtility(req, res, this.configs.get('traps.types'))
|
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()
|
if ( !req.trap.has_trap() ) return next()
|
||||||
else if ( req.trap.allows(req.path) ) return next()
|
else if ( req.trap.allows(req.path) ) return next()
|
||||||
else return req.trap.redirect()
|
else return req.trap.redirect()
|
||||||
|
@ -36,6 +36,14 @@ const auth_routes = {
|
|||||||
['middleware::api:Permission', { check: 'v1:auth:users:get' }],
|
['middleware::api:Permission', { check: 'v1:auth:users:get' }],
|
||||||
'controller::api:v1:Auth.get_user',
|
'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': [
|
'/groups/:id': [
|
||||||
'middleware::auth:APIRoute',
|
'middleware::auth:APIRoute',
|
||||||
['middleware::api:Permission', { check: 'v1:auth:groups:get' }],
|
['middleware::api:Permission', { check: 'v1:auth:groups:get' }],
|
||||||
|
@ -14,6 +14,14 @@ const iam_routes = {
|
|||||||
['middleware::api:Permission', { check: 'v1:iam:policy:get' }],
|
['middleware::api:Permission', { check: 'v1:iam:policy:get' }],
|
||||||
'controller::api:v1:IAM.get_policy',
|
'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: {
|
post: {
|
||||||
@ -21,6 +29,10 @@ const iam_routes = {
|
|||||||
['middleware::api:Permission', { check: 'v1:iam:policy:create' }],
|
['middleware::api:Permission', { check: 'v1:iam:policy:create' }],
|
||||||
'controller::api:v1:IAM.create_policy',
|
'controller::api:v1:IAM.create_policy',
|
||||||
],
|
],
|
||||||
|
'/permission': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:iam:permission:create' }],
|
||||||
|
'controller::api:v1:IAM.create_permission',
|
||||||
|
],
|
||||||
'/check_entity_access': [
|
'/check_entity_access': [
|
||||||
['middleware::api:Permission', { check: 'v1:iam:check_entity_access' }],
|
['middleware::api:Permission', { check: 'v1:iam:check_entity_access' }],
|
||||||
'controller::api: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' }],
|
['middleware::api:Permission', { check: 'v1:iam:policy:update' }],
|
||||||
'controller::api:v1:IAM.update_policy',
|
'controller::api:v1:IAM.update_policy',
|
||||||
],
|
],
|
||||||
|
'/permission/:id': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:iam:permission:update' }],
|
||||||
|
'controller::api:v1:IAM.update_permission',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: {
|
delete: {
|
||||||
@ -43,6 +59,10 @@ const iam_routes = {
|
|||||||
['middleware::api:Permission', { check: 'v1:iam:policy:delete' }],
|
['middleware::api:Permission', { check: 'v1:iam:policy:delete' }],
|
||||||
'controller::api:v1:IAM.delete_policy',
|
'controller::api:v1:IAM.delete_policy',
|
||||||
],
|
],
|
||||||
|
'/permission/:id': [
|
||||||
|
['middleware::api:Permission', { check: 'v1:iam:permission:delete' }],
|
||||||
|
'controller::api:v1:IAM.delete_permission',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,22 @@ const ldap_routes = {
|
|||||||
['middleware::api:Permission', { check: 'v1:ldap:groups:get' }],
|
['middleware::api:Permission', { check: 'v1:ldap:groups:get' }],
|
||||||
'controller::api:v1:LDAP.get_group',
|
'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': [
|
'/config': [
|
||||||
['middleware::api:Permission', { check: 'v1:ldap:config:get' }],
|
['middleware::api:Permission', { check: 'v1:ldap:config:get' }],
|
||||||
'controller::api:v1:LDAP.get_config',
|
'controller::api:v1:LDAP.get_config',
|
||||||
@ -37,6 +53,14 @@ const ldap_routes = {
|
|||||||
['middleware::api:Permission', { check: 'v1:ldap:groups:create' }],
|
['middleware::api:Permission', { check: 'v1:ldap:groups:create' }],
|
||||||
'controller::api:v1:LDAP.create_group',
|
'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: {
|
patch: {
|
||||||
@ -48,6 +72,14 @@ const ldap_routes = {
|
|||||||
['middleware::api:Permission', { check: 'v1:ldap:groups:update' }],
|
['middleware::api:Permission', { check: 'v1:ldap:groups:update' }],
|
||||||
'controller::api:v1:LDAP.update_group',
|
'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: {
|
delete: {
|
||||||
@ -59,6 +91,14 @@ const ldap_routes = {
|
|||||||
['middleware::api:Permission', { check: 'v1:ldap:groups:delete' }],
|
['middleware::api:Permission', { check: 'v1:ldap:groups:delete' }],
|
||||||
'controller::api:v1:LDAP.delete_group',
|
'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',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
48
app/routing/routers/api/v1/radius.routes.js
Normal file
48
app/routing/routers/api/v1/radius.routes.js
Normal 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
|
@ -67,6 +67,21 @@ const index = {
|
|||||||
'controller::auth:Forms.logout_provider_present_success',
|
'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': [
|
'/login-message': [
|
||||||
'middleware::auth:UserOnly',
|
'middleware::auth:UserOnly',
|
||||||
'controller::api:v1:System.show_login_message',
|
'controller::api:v1:System.show_login_message',
|
||||||
|
@ -7,6 +7,9 @@ const openid = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
get: {
|
get: {
|
||||||
|
'/grant-and-save/:app_id/:uid': [
|
||||||
|
'middleware::auth:UserOnly', 'controller::OpenID.grant_and_save',
|
||||||
|
],
|
||||||
'/interaction/:uid': [
|
'/interaction/:uid': [
|
||||||
'controller::OpenID.handle_interaction',
|
'controller::OpenID.handle_interaction',
|
||||||
],
|
],
|
||||||
|
@ -2,6 +2,7 @@ const Unit = require('libflitter/Unit')
|
|||||||
const LDAP = require('ldapjs')
|
const LDAP = require('ldapjs')
|
||||||
const Validator = require('email-validator')
|
const Validator = require('email-validator')
|
||||||
const net = require('net')
|
const net = require('net')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
// TODO support logging ALL ldap requests when in DEBUG, not just routed ones
|
// TODO support logging ALL ldap requests when in DEBUG, not just routed ones
|
||||||
// TODO need to support LDAP server auto-discovery/detection features
|
// 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)
|
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.
|
* Get the anonymous DN.
|
||||||
* @returns {ldap/DN}
|
* @returns {ldap/DN}
|
||||||
@ -77,7 +90,11 @@ class LDAPServerUnit extends Unit {
|
|||||||
|
|
||||||
// If Flitter is configured to use an SSL certificate,
|
// If Flitter is configured to use an SSL certificate,
|
||||||
// use it to enable LDAPS in the server.
|
// 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.')
|
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.certificate = await this.express.ssl_certificate()
|
||||||
server_config.key = await this.express.ssl_key()
|
server_config.key = await this.express.ssl_key()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
const fs = require('fs')
|
||||||
const Unit = require('libflitter/Unit')
|
const Unit = require('libflitter/Unit')
|
||||||
const { Provider, interactionPolicy: { Prompt, base: policy } } = require('oidc-provider')
|
const { Provider, interactionPolicy: { Prompt, base: policy } } = require('oidc-provider')
|
||||||
const uuid = require('uuid').v4
|
const uuid = require('uuid').v4
|
||||||
@ -14,6 +15,15 @@ class OpenIDConnectUnit extends Unit {
|
|||||||
return [...super.services, 'output', 'configs', 'models']
|
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) {
|
async go(app) {
|
||||||
this.Vue = this.app.di().get('Vue')
|
this.Vue = this.app.di().get('Vue')
|
||||||
const issuer = this.configs.get('app.url')
|
const issuer = this.configs.get('app.url')
|
||||||
@ -23,9 +33,13 @@ class OpenIDConnectUnit extends Unit {
|
|||||||
|
|
||||||
CoreIDAdapter.connect(app)
|
CoreIDAdapter.connect(app)
|
||||||
|
|
||||||
|
const jwks_file = this.configs.get('oidc.jwks_file')
|
||||||
|
const jwks = this.load_jwks(jwks_file)
|
||||||
|
|
||||||
this.provider = new Provider(issuer, {
|
this.provider = new Provider(issuer, {
|
||||||
adapter: CoreIDAdapter,
|
adapter: CoreIDAdapter,
|
||||||
clients: [],
|
clients: [],
|
||||||
|
jwks,
|
||||||
interactions: {
|
interactions: {
|
||||||
interactions,
|
interactions,
|
||||||
url: (ctx, interaction) => `/openid/interaction/${ctx.oidc.uid.toLowerCase()}`,
|
url: (ctx, interaction) => `/openid/interaction/${ctx.oidc.uid.toLowerCase()}`,
|
||||||
@ -58,6 +72,15 @@ class OpenIDConnectUnit extends Unit {
|
|||||||
...configuration,
|
...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
|
if ( configuration.proxy ) this.provider.proxy = true
|
||||||
app.express.use('/oidc', this.wrap(this.provider.callback))
|
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)
|
return callback(req, res, next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user