Compare commits
79 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 |
142
.drone.yml
142
.drone.yml
@ -1,88 +1,68 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: default
|
||||
type: kubernetes
|
||||
name: build
|
||||
|
||||
metadata:
|
||||
labels:
|
||||
pod-security.kubernetes.io/audit: privileged
|
||||
|
||||
services:
|
||||
- name: docker daemon
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
environment:
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
|
||||
steps:
|
||||
- name: release
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: gitea_api_key
|
||||
base_url: https://code.garrettmills.dev
|
||||
checksum: md5
|
||||
title: ${DRONE_TAG}
|
||||
- name: container build
|
||||
image: docker:latest
|
||||
privileged: true
|
||||
commands:
|
||||
- "while ! docker stats --no-stream; do sleep 1; done"
|
||||
- "docker build -t $DOCKER_REGISTRY/starship/coreid ."
|
||||
- "docker push $DOCKER_REGISTRY/starship/coreid"
|
||||
environment:
|
||||
DOCKER_HOST: tcp://localhost:2375
|
||||
DOCKER_REGISTRY:
|
||||
from_secret: DOCKER_REGISTRY
|
||||
|
||||
- name: environment substitution
|
||||
image: rockylinux:9.0-minimal
|
||||
commands:
|
||||
- microdnf install -y gettext
|
||||
- cd deploy && mkdir ../deploy-subst && bash -c 'for f in *.yaml; do envsubst < $f > ../deploy-subst/$f; done'
|
||||
environment:
|
||||
COREID_DOMAIN:
|
||||
from_secret: COREID_DOMAIN
|
||||
DOCKER_REGISTRY:
|
||||
from_secret: DOCKER_REGISTRY
|
||||
COREID_DATABASE_HOST:
|
||||
from_secret: COREID_DATABASE_HOST
|
||||
COREID_DATABASE_NAME:
|
||||
from_secret: COREID_DATABASE_NAME
|
||||
COREID_LDAP_BASE_DC:
|
||||
from_secret: COREID_LDAP_BASE_DC
|
||||
COREID_REDIS_HOST:
|
||||
from_secret: COREID_REDIS_HOST
|
||||
COREID_SMTP_HOST:
|
||||
from_secret: COREID_SMTP_HOST
|
||||
when:
|
||||
event: tag
|
||||
- name: deploy to production
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host:
|
||||
from_secret: deploy_ssh_host
|
||||
username:
|
||||
from_secret: deploy_ssh_user
|
||||
key:
|
||||
from_secret: deploy_ssh_key
|
||||
port:
|
||||
from_secret: deploy_ssh_port
|
||||
script:
|
||||
- cd /home/coreid/CoreID
|
||||
- git checkout master
|
||||
- git pull
|
||||
- git checkout ${DRONE_TAG}
|
||||
- git pull
|
||||
- yarn install
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
- promote
|
||||
- name: restart production services
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host:
|
||||
from_secret: deploy_ssh_host
|
||||
username:
|
||||
from_secret: deploy_ssh_admin_user
|
||||
key:
|
||||
from_secret: deploy_ssh_key
|
||||
port:
|
||||
from_secret: deploy_ssh_port
|
||||
script:
|
||||
- systemctl restart coreid-www
|
||||
- systemctl restart coreid-jobs
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
- promote
|
||||
- name: send success notifications
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: notify_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"title": "Drone-CI [Starship/CoreID]",
|
||||
"message": "Build ${DRONE_BUILD_NUMBER} promoted to production.",
|
||||
"priority": 4
|
||||
}
|
||||
when:
|
||||
status: success
|
||||
event:
|
||||
- tag
|
||||
- promote
|
||||
- name: send error notifications
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: notify_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"title": "Drone-CI [Starship/CoreID]",
|
||||
"message": "An error was encountered while promoting build ${DRONE_BUILD_NUMBER} to production.",
|
||||
"priority": 6
|
||||
}
|
||||
when:
|
||||
status: failure
|
||||
event:
|
||||
- tag
|
||||
- promote
|
||||
|
||||
- name: k8s rollout
|
||||
image: bitnami/kubectl
|
||||
privileged: true
|
||||
commands:
|
||||
- cd deploy-subst && kubectl apply -f .
|
||||
- kubectl rollout restart deployment/coreid-www -n starship
|
||||
- kubectl rollout restart deployment/coreid-jobs -n starship
|
||||
depends_on:
|
||||
- container build
|
||||
- environment substitution
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
- promote
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
*.conf
|
||||
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
@ -150,3 +152,4 @@ tmp.uploads/*
|
||||
!tmp.uploads/.gitkeep
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
ttls-pap.conf
|
||||
|
16
Dockerfile
Normal file
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'),
|
||||
'LDAPRoutingUnit': require('./app/unit/LDAPRoutingUnit'),
|
||||
'OpenIDConnect' : require('./app/unit/OpenIDConnectUnit'),
|
||||
'Radius' : require('./app/unit/RadiusUnit'),
|
||||
|
||||
/*
|
||||
* 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 |
@ -28,7 +28,7 @@ const template = `
|
||||
v-if="field.type === 'display' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))"
|
||||
v-html="typeof field.display === 'function' ? field.display(data) : field.display"
|
||||
></span>
|
||||
<span v-if="field.type.startsWith('select') && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
||||
<span v-if="field.type.startsWith('select') && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data, field.options))">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<select
|
||||
:id="uuid+field.field"
|
||||
@ -42,13 +42,13 @@ const template = `
|
||||
<option
|
||||
v-for="option of field.options"
|
||||
:value="option.value"
|
||||
:selected="data[field.field] && data[field.field].includes(option.value)"
|
||||
:selected="data[field.field] && (data[field.field] === option.value || (Array.isArray(data[field.field]) && data[field.field].includes(option.value)))"
|
||||
>{{ typeof option.display === 'function' ? option.display(option) : option.display }}</option>
|
||||
</select>
|
||||
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||
</span>
|
||||
<span v-if="field.type === 'text' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<label :for="uuid+field.field" style="display: inline">{{ field.name }} <span v-if="field.help" :title="field.help"><img src="/assets/info-circle-solid.svg" height="18"></span></label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
|
@ -8,6 +8,8 @@ import AppSetupComponent from './dash/AppSetup.component.js'
|
||||
|
||||
import ListingComponent from './cobalt/Listing.component.js'
|
||||
import FormComponent from './cobalt/Form.component.js'
|
||||
import RootPageComponent from './dash/RootPage.component.js'
|
||||
import OutletComponent from './dash/Outlet.component.js'
|
||||
|
||||
import { T } from './service/Translate.service.js'
|
||||
|
||||
@ -22,6 +24,8 @@ const dash_components = {
|
||||
|
||||
ListingComponent,
|
||||
FormComponent,
|
||||
RootPageComponent,
|
||||
OutletComponent,
|
||||
}
|
||||
|
||||
export { dash_components }
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component } from '../../lib/vues6/vues6.js'
|
||||
import { event_bus } from '../service/EventBus.service.js'
|
||||
import { session } from '../service/Session.service.js'
|
||||
import { action_service } from '../service/Action.service.js'
|
||||
|
||||
const template = `
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
|
||||
@ -35,9 +36,9 @@ const template = `
|
||||
aria-labelledby="navbarDropdown"
|
||||
>
|
||||
<h6 class="dropdown-header">Hello, {{ first_name }}.</h6>
|
||||
<a href="/dash/profile" class="dropdown-item">My Profile</a>
|
||||
<a href="/dash/c/listing/reflect/Token" v-if="can.api_tokens" class="dropdown-item">API Tokens</a>
|
||||
<a href="/dash/c/listing/system/Announcement" v-if="can.messages" class="dropdown-item">System Announcements</a>
|
||||
<a href="/dash/profile" class="dropdown-item" @click="navigate('dash.profile')" onclick="return false;">My Profile</a>
|
||||
<a href="/dash/c/listing/reflect/Token" v-if="can.api_tokens" @click="cobalt('reflect/Token', 'list')" class="dropdown-item" onclick="return false;">API Tokens</a>
|
||||
<a href="/dash/c/listing/system/Announcement" v-if="can.messages" @click="cobalt('system/Announcement', 'list')" class="dropdown-item" onclick="return false;">System Announcements</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="/auth/logout" class="dropdown-item">Sign-Out of {{ app_name }}</a>
|
||||
</div>
|
||||
@ -71,4 +72,20 @@ export default class NavBarComponent extends Component {
|
||||
toggle_sidebar() {
|
||||
this.toggle_event.fire()
|
||||
}
|
||||
|
||||
navigate(page) {
|
||||
action_service.perform({
|
||||
action: 'navigate',
|
||||
page,
|
||||
})
|
||||
}
|
||||
|
||||
cobalt(resource, action, id = undefined) {
|
||||
action_service.perform({
|
||||
type: 'resource',
|
||||
resource,
|
||||
action,
|
||||
id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
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()
|
||||
}
|
||||
}
|
@ -33,8 +33,8 @@ export default class SideBarComponent extends Component {
|
||||
this.possible_actions = [
|
||||
{
|
||||
text: 'Profile',
|
||||
action: 'redirect',
|
||||
next: '/dash/profile',
|
||||
action: 'navigate',
|
||||
page: 'dash.profile',
|
||||
},
|
||||
{
|
||||
text: 'Users',
|
||||
@ -60,6 +60,24 @@ export default class SideBarComponent extends Component {
|
||||
type: 'resource',
|
||||
resource: 'iam/Policy',
|
||||
},
|
||||
{
|
||||
text: 'IAM Permissions',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'iam/Permission',
|
||||
},
|
||||
{
|
||||
text: 'Computers',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'ldap/Machine',
|
||||
},
|
||||
{
|
||||
text: 'Computer Groups',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'ldap/MachineGroup',
|
||||
},
|
||||
{
|
||||
text: 'LDAP Clients',
|
||||
action: 'list',
|
||||
@ -72,6 +90,12 @@ export default class SideBarComponent extends Component {
|
||||
type: 'resource',
|
||||
resource: 'oauth/Client',
|
||||
},
|
||||
{
|
||||
text: 'RADIUS Clients',
|
||||
action: 'list',
|
||||
type: 'resource',
|
||||
resource: 'radius/Client',
|
||||
},
|
||||
{
|
||||
text: 'OpenID Connect Clients',
|
||||
action: 'list',
|
||||
|
@ -8,7 +8,7 @@ import { utility } from '../../service/Utility.service.js'
|
||||
import { profile_service } from '../../service/Profile.service.js'
|
||||
|
||||
const template = `
|
||||
<div class="coreid-profile-container mb-5">
|
||||
<div class="coreid-profile-container mb-5 offset-0 col-md-8 offset-md-2 col-xl-6 offset-xl-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
@ -77,6 +77,20 @@ const template = `
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h4 style="margin-left: 15px">{{ t['profile.advanced_header'] }}</h4>
|
||||
<div class="col-12 form-group">
|
||||
<label for="coreid-profile-shell-input">{{ t['profile.advanced_shell'] }}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="coreid-profile-shell-input"
|
||||
v-model="profile_shell"
|
||||
@keyup="on_key_up($event)"
|
||||
placeholder="/bin/bash"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item text-right font-italic text-muted">
|
||||
{{ form_message }}
|
||||
@ -90,6 +104,11 @@ const template = `
|
||||
@click="change_password"
|
||||
>{{ t['password.change'] }}</button>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<h4>{{ t['authn.authn'] }}</h4>
|
||||
<p>{{ t['authn.desc'].replace(/APP_NAME/g, app_name) }}</p>
|
||||
<button class="btn btn-success btn-sm" type="button">{{ t['authn.enable'] }}</button>
|
||||
</li>
|
||||
<li class="list-group-item" v-if="ready && !has_mfa && (!user_id || user_id === 'me')">
|
||||
<h4>{{ t['mfa.mfa'] }}</h4>
|
||||
<p>{{ t['profile.mfa_1'].replace(/APP_NAME/g, app_name) }}</p>
|
||||
@ -117,6 +136,7 @@ const template = `
|
||||
<div class="col-9">
|
||||
{{ pw.name }}
|
||||
<br><span class="text-muted font-italic">{{ t['profile.issued'] }} {{ pw.created }}</span>
|
||||
<span class="text-muted font-italic"> | {{ t['profile.accessed'] }} {{ pw.accessed || t['common.never'] }}</span>
|
||||
</div>
|
||||
<div class="col-3 my-auto">
|
||||
<button
|
||||
@ -202,6 +222,7 @@ export default class EditProfileComponent extends Component {
|
||||
this.profile_last = ''
|
||||
this.profile_email = ''
|
||||
this.profile_tagline = ''
|
||||
this.profile_shell = ''
|
||||
this.last_reset = ''
|
||||
this.mfa_enable_date = ''
|
||||
|
||||
@ -271,7 +292,14 @@ export default class EditProfileComponent extends Component {
|
||||
'profile.app_key',
|
||||
'profile.example_gateway_url',
|
||||
'profile.save_notify',
|
||||
'profile.test_notify'
|
||||
'profile.test_notify',
|
||||
'profile.advanced_header',
|
||||
'profile.advanced_shell',
|
||||
'profile.accessed',
|
||||
'common.never',
|
||||
'authn.authn',
|
||||
'authn.desc',
|
||||
'authn.enable',
|
||||
)
|
||||
|
||||
this.app_name = session.get('app.name')
|
||||
@ -292,6 +320,7 @@ export default class EditProfileComponent extends Component {
|
||||
last_name: this.profile_last,
|
||||
email: this.profile_email,
|
||||
tagline: this.profile_tagline,
|
||||
login_shell: this.profile_shell,
|
||||
user_id: this.user_id || 'me',
|
||||
}
|
||||
}
|
||||
@ -340,6 +369,7 @@ export default class EditProfileComponent extends Component {
|
||||
this.profile_last = result.last_name
|
||||
this.profile_email = result.email
|
||||
this.profile_tagline = result.tagline
|
||||
this.profile_shell = result.login_shell
|
||||
|
||||
const notify_config = await profile_service.get_notify(this.user_id || 'me')
|
||||
if ( !notify_config || !notify_config.has_config ) {
|
||||
@ -384,6 +414,7 @@ export default class EditProfileComponent extends Component {
|
||||
this.app_passwords = app_pws.map(x => {
|
||||
if ( x.expires ) x.expires = (new Date(x.expires)).toLocaleDateString()
|
||||
if ( x.created ) x.created = (new Date(x.created)).toLocaleDateString()
|
||||
if ( x.accessed ) x.accessed = (new Date(x.accessed)).toLocaleDateString()
|
||||
return x
|
||||
})
|
||||
}
|
||||
|
@ -40,10 +40,10 @@ class AppResource extends CRUDBase {
|
||||
},
|
||||
{
|
||||
position: 'main',
|
||||
action: 'redirect',
|
||||
action: 'navigate',
|
||||
text: 'Setup Wizard',
|
||||
color: 'success',
|
||||
next: '/dash/app/setup',
|
||||
page: 'app.setup',
|
||||
},
|
||||
{
|
||||
type: 'resource',
|
||||
@ -84,6 +84,14 @@ class AppResource extends CRUDBase {
|
||||
field: 'description',
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
name: 'IAM Target',
|
||||
field: 'id',
|
||||
type: 'text',
|
||||
readonly: true,
|
||||
hidden: ['insert'],
|
||||
help: `(LDAP use) Allows restricting users to only those that can access this application. (filter: iamTarget)`,
|
||||
},
|
||||
{
|
||||
name: 'Associated LDAP Clients',
|
||||
field: 'ldap_client_ids',
|
||||
@ -104,6 +112,16 @@ class AppResource extends CRUDBase {
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Associated RADIUS Clients',
|
||||
field: 'radius_client_ids',
|
||||
type: 'select.dynamic.multiple',
|
||||
options: {
|
||||
resource: 'radius/Client',
|
||||
display: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Associated OpenID Connect Clients',
|
||||
field: 'openid_client_ids',
|
||||
|
@ -62,6 +62,15 @@ class GroupResource extends CRUDBase {
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
/*{
|
||||
name: 'Superuser equivalent?',
|
||||
field: 'grants_sudo',
|
||||
type: 'select',
|
||||
options: [
|
||||
{display: 'Yes', value: true},
|
||||
{display: 'No', value: false},
|
||||
],
|
||||
},*/
|
||||
{
|
||||
name: 'Users',
|
||||
field: 'user_ids',
|
||||
|
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 }
|
@ -41,6 +41,11 @@ class PolicyResource extends CRUDBase {
|
||||
name: 'Target',
|
||||
field: 'target_display',
|
||||
},
|
||||
{
|
||||
name: 'Permission',
|
||||
field: 'permission',
|
||||
renderer: permission => permission || '-',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
@ -122,6 +127,8 @@ class PolicyResource extends CRUDBase {
|
||||
options: [
|
||||
{display: 'Application', value: 'application'},
|
||||
{display: 'API Scope', value: 'api_scope'},
|
||||
{display: 'Computer', value: 'machine'},
|
||||
{display: 'Computer Group', value: 'machine_group'},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -148,6 +155,94 @@ class PolicyResource extends CRUDBase {
|
||||
},
|
||||
if: (form_data) => form_data.target_type === 'api_scope'
|
||||
},
|
||||
{
|
||||
name: 'Target',
|
||||
field: 'target_id',
|
||||
required: true,
|
||||
type: 'select.dynamic',
|
||||
options: {
|
||||
resource: 'ldap/Machine',
|
||||
display: machine => `${machine.name}${machine.host_name ? ' (' + machine.host_name + ')' : ''}`,
|
||||
value: 'id',
|
||||
},
|
||||
if: (form_data) => form_data.target_type === 'machine'
|
||||
},
|
||||
{
|
||||
name: 'Target',
|
||||
field: 'target_id',
|
||||
required: true,
|
||||
type: 'select.dynamic',
|
||||
options: {
|
||||
resource: 'ldap/MachineGroup',
|
||||
display: group => `${group.name} (${(group.machine_ids || []).length} computers)`,
|
||||
value: 'id',
|
||||
},
|
||||
if: (form_data) => form_data.target_type === 'machine_group'
|
||||
},
|
||||
{
|
||||
name: 'Permission',
|
||||
field: 'permission',
|
||||
required: false,
|
||||
type: 'select.dynamic',
|
||||
options: {
|
||||
resource: 'iam/Permission',
|
||||
display: 'permission',
|
||||
value: 'permission',
|
||||
other_params: {
|
||||
target_type: 'application',
|
||||
include_unset: true,
|
||||
},
|
||||
},
|
||||
if: (form_data, opts) => form_data.target_type === 'application' && opts?.length
|
||||
},
|
||||
{
|
||||
name: 'Permission',
|
||||
field: 'permission',
|
||||
required: false,
|
||||
type: 'select.dynamic',
|
||||
options: {
|
||||
resource: 'iam/Permission',
|
||||
display: 'permission',
|
||||
value: 'permission',
|
||||
other_params: {
|
||||
target_type: 'api_scope',
|
||||
include_unset: true,
|
||||
},
|
||||
},
|
||||
if: (form_data, opts) => form_data.target_type === 'api_scope' && opts?.length
|
||||
},
|
||||
{
|
||||
name: 'Permission',
|
||||
field: 'permission',
|
||||
required: false,
|
||||
type: 'select.dynamic',
|
||||
options: {
|
||||
resource: 'iam/Permission',
|
||||
display: 'permission',
|
||||
value: 'permission',
|
||||
other_params: {
|
||||
target_type: 'machine',
|
||||
include_unset: true,
|
||||
},
|
||||
},
|
||||
if: (form_data, opts) => form_data.target_type === 'machine' && opts?.length
|
||||
},
|
||||
{
|
||||
name: 'Permission',
|
||||
field: 'permission',
|
||||
required: false,
|
||||
type: 'select.dynamic',
|
||||
options: {
|
||||
resource: 'iam/Permission',
|
||||
display: 'permission',
|
||||
value: 'permission',
|
||||
other_params: {
|
||||
target_type: 'machine_group',
|
||||
include_unset: true,
|
||||
},
|
||||
},
|
||||
if: (form_data, opts) => form_data.target_type === 'machine_group' && opts?.length
|
||||
},
|
||||
],
|
||||
/*handlers: {
|
||||
insert: {
|
||||
|
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 }
|
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,5 +1,11 @@
|
||||
import { location_service } from './Location.service.js'
|
||||
import { resource_service } from './Resource.service.js'
|
||||
import { event_bus } from './EventBus.service.js'
|
||||
|
||||
const pageMap = {
|
||||
'dash.profile': '/dash/profile',
|
||||
'app.setup': '/dash/app/setup',
|
||||
}
|
||||
|
||||
class ActionService {
|
||||
async perform({ text = '', action, ...args }) {
|
||||
@ -7,21 +13,44 @@ class ActionService {
|
||||
if ( args.next ) {
|
||||
return location_service.redirect(args.next, args.delay || 0)
|
||||
}
|
||||
} else if ( action === 'navigate' ) {
|
||||
if ( args.page && pageMap[args.page] ) {
|
||||
window.history.pushState('pageNavigate', `Open ${args.page}`, pageMap[args.page])
|
||||
return event_bus.event('root.navigate').fire(args)
|
||||
}
|
||||
} else if ( action === 'back' ) {
|
||||
return location_service.back()
|
||||
} else if ( args.type === 'resource' ) {
|
||||
const { resource } = args
|
||||
if ( action === 'insert' ) {
|
||||
return location_service.redirect(`/dash/c/form/${resource}`, 0)
|
||||
window.history.pushState('cobaltForm', `Insert ${resource}`, `/dash/c/form/${resource}`)
|
||||
|
||||
return event_bus.event('root.navigate').fire({
|
||||
page: 'cobalt.form',
|
||||
resource,
|
||||
mode: 'insert',
|
||||
})
|
||||
} else if ( action === 'update' ) {
|
||||
const { id } = args
|
||||
return location_service.redirect(`/dash/c/form/${resource}?id=${id}`, 0)
|
||||
window.history.pushState('cobaltForm', `Edit ${resource}`, `/dash/c/form/${resource}?id=${id}`)
|
||||
|
||||
return event_bus.event('root.navigate').fire({
|
||||
page: 'cobalt.form',
|
||||
resource,
|
||||
mode: 'update',
|
||||
form_id: id,
|
||||
})
|
||||
} else if ( action === 'delete' ) {
|
||||
const { id } = args
|
||||
const rsc = await resource_service.get(resource)
|
||||
await rsc.delete(id)
|
||||
} else if ( action === 'list' ) {
|
||||
return location_service.redirect(`/dash/c/listing/${resource}`, 0)
|
||||
window.history.pushState('cobaltListing', `View ${resource}`, `/dash/c/listing/${resource}`)
|
||||
|
||||
return event_bus.event('root.navigate').fire({
|
||||
page: 'cobalt.listing',
|
||||
resource,
|
||||
})
|
||||
}
|
||||
} else if ( action === 'post' ) {
|
||||
const inputs = []
|
||||
|
@ -1,3 +1,5 @@
|
||||
import {message_service} from './Message.service.js'
|
||||
|
||||
class ProfileService {
|
||||
|
||||
async get_profile(user_id = 'me') {
|
||||
@ -10,8 +12,11 @@ class ProfileService {
|
||||
if ( results && results.data && results.data.data ) return results.data.data
|
||||
}
|
||||
|
||||
async update_profile({ user_id, first_name, last_name, email, tagline = undefined }) {
|
||||
await axios.patch(`/api/v1/profile/${user_id}`, { first_name, last_name, email, tagline })
|
||||
async update_profile({ user_id, first_name, last_name, email, login_shell = undefined, tagline = undefined }) {
|
||||
const results = await axios.patch(`/api/v1/profile/${user_id}`, { first_name, last_name, email, tagline, login_shell })
|
||||
if ( results && results.data && results.data.data && results.data.data.force_message_refresh ) {
|
||||
await message_service._listener_tick()
|
||||
}
|
||||
}
|
||||
|
||||
async update_notify({ user_id = 'me', app_key, gateway_url }) {
|
||||
|
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 |
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 = await Client.findById(req.params.id)
|
||||
|
||||
if ( !client || !client.active )
|
||||
if ( !client )
|
||||
return res.status(404)
|
||||
.message(req.T('api.client_not_found'))
|
||||
.api()
|
||||
@ -153,6 +153,12 @@ class OpenIDController extends Controller {
|
||||
})
|
||||
}
|
||||
|
||||
// If the user has already authorized this app, just redirect
|
||||
if ( req.user.has_authorized({ id: application.id }) ) {
|
||||
return res.redirect(`/openid/interaction/${uid.toLowerCase()}/grant`)
|
||||
}
|
||||
|
||||
// Otherwise, prompt them for authorization
|
||||
return res.page('public:message', {
|
||||
...this.Vue.data({
|
||||
message: `<h3 class="font-weight-light">Authorize ${application.name}?</h3>
|
||||
@ -170,6 +176,11 @@ class OpenIDController extends Controller {
|
||||
{
|
||||
text: req.T('common.grant'),
|
||||
action: 'redirect',
|
||||
next: `/openid/grant-and-save/${application.id}/${uid.toLowerCase()}`,
|
||||
},
|
||||
{
|
||||
text: req.T('common.grant_once'),
|
||||
action: 'redirect',
|
||||
next: `/openid/interaction/${uid.toLowerCase()}/grant`,
|
||||
},
|
||||
],
|
||||
@ -177,6 +188,19 @@ class OpenIDController extends Controller {
|
||||
})
|
||||
}
|
||||
|
||||
async grant_and_save(req, res, next) {
|
||||
if ( !req.user.has_authorized({ id: req.params.app_id }) ) {
|
||||
req.user.authorize({
|
||||
id: req.params.app_id,
|
||||
api_scopes: ['openid-connect'],
|
||||
})
|
||||
|
||||
await req.user.save()
|
||||
}
|
||||
|
||||
return res.redirect(`/openid/interaction/${req.params.uid.toLowerCase()}/grant`)
|
||||
}
|
||||
|
||||
async login(req, res, { uid, prompt, params, session }) {
|
||||
return res.redirect(`/openid/interaction/${uid.toLowerCase()}/start-session`)
|
||||
}
|
||||
|
@ -115,6 +115,28 @@ class AppController extends Controller {
|
||||
application.oauth_client_ids = oauth_client_ids
|
||||
}
|
||||
|
||||
// Verify RADIUS client IDs
|
||||
const RadiusClient = this.models.get('radius:Client')
|
||||
if ( req.body.radius_client_ids ) {
|
||||
const parsed = typeof req.body.radius_client_ids === 'string' ? this.utility.infer(req.body.radius_client_ids) : req.body.radius_client_ids
|
||||
const radius_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of radius_client_ids ) {
|
||||
const client = await RadiusClient.findById(id)
|
||||
if ( !client || !client.active || !req.user.can(`radius:client:${client.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.invalid_radius_client_id')} ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ radius_client_ids: client.id })
|
||||
if ( other_assoc_app )
|
||||
return res.status(400) // TODO translate this
|
||||
.message(`The RADIUS client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.radius_client_ids = radius_client_ids
|
||||
}
|
||||
|
||||
// Verify OpenID client IDs
|
||||
const OpenIDClient = this.models.get('openid:Client')
|
||||
if ( req.body.openid_client_ids ) {
|
||||
@ -242,6 +264,28 @@ class AppController extends Controller {
|
||||
application.oauth_client_ids = oauth_client_ids
|
||||
} else application.oauth_client_ids = []
|
||||
|
||||
// Verify OAuth client IDs
|
||||
const RadiusClient = this.models.get('radius:Client')
|
||||
if ( req.body.radius_client_ids ) {
|
||||
const parsed = typeof req.body.radius_client_ids === 'string' ? this.utility.infer(req.body.radius_client_ids) : req.body.radius_client_ids
|
||||
const radius_client_ids = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for ( const id of radius_client_ids ) {
|
||||
const client = await RadiusClient.findById(id)
|
||||
if ( !client || !client.active || !req.user.can(`radius:client:${client.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.invalid_radius_client_id')} ${id}`)
|
||||
.api()
|
||||
|
||||
const other_assoc_app = await Application.findOne({ radius_client_ids: client.id })
|
||||
if ( other_assoc_app && other_assoc_app.id !== application.id )
|
||||
return res.status(400) // TODO translate this
|
||||
.message(`The RADIUS client ${client.name} is already associated with an existing application (${other_assoc_app.name}).`)
|
||||
.api()
|
||||
}
|
||||
|
||||
application.radius_client_ids = radius_client_ids
|
||||
} else application.radius_client_ids = []
|
||||
|
||||
// Verify OpenID client IDs
|
||||
const OpenIDClient = this.models.get('openid:Client')
|
||||
if ( req.body.openid_client_ids ) {
|
||||
|
@ -91,6 +91,7 @@ class AuthController extends Controller {
|
||||
if ( !(await User.findOne()) ) user.promote('root')
|
||||
|
||||
await user.save()
|
||||
await user.grant_defaults()
|
||||
|
||||
// Log in the user automatically
|
||||
await this.auth.get_provider().session(req, user)
|
||||
@ -219,6 +220,48 @@ class AuthController extends Controller {
|
||||
return res.api(await user.to_api())
|
||||
}
|
||||
|
||||
async get_user_flat(req, res, next) {
|
||||
if ( req.params.id === 'me' )
|
||||
return res.json(await req.user.to_api())
|
||||
|
||||
const User = this.models.get('auth:User')
|
||||
const user = await User.findById(req.params.id)
|
||||
|
||||
if ( !user )
|
||||
return res.status(404)
|
||||
.message(req.T('api.user_not_found'))
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`auth:user:${user.id}:view`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
return res.json(await user.to_api())
|
||||
}
|
||||
|
||||
async get_user_photo(req, res, next) {
|
||||
let user
|
||||
if ( req.params.id === 'me' ) {
|
||||
user = req.user
|
||||
} else {
|
||||
const User = this.models.get('auth:User')
|
||||
user = await User.findOne({ uid: req.params.id })
|
||||
}
|
||||
|
||||
if ( !user )
|
||||
return res.status(404)
|
||||
.message(req.T('api.user_not_found'))
|
||||
.api()
|
||||
|
||||
const file = await user.photo()
|
||||
if ( !file )
|
||||
// The user does not have a profile. Send the default.
|
||||
return res.sendFile(this.utility.path('app/assets/people.png'))
|
||||
|
||||
await file.send(res)
|
||||
}
|
||||
|
||||
async create_group(req, res, next) {
|
||||
if ( !req.user.can(`auth:group:create`) )
|
||||
return res.status(401)
|
||||
@ -239,7 +282,10 @@ class AuthController extends Controller {
|
||||
.message(req.T('api.group_already_exists'))
|
||||
.api()
|
||||
|
||||
const group = new Group({ name: req.body.name })
|
||||
const group = new Group({
|
||||
name: req.body.name,
|
||||
grants_sudo: !!req.body.grants_sudo,
|
||||
})
|
||||
|
||||
// Validate user ids
|
||||
const User = this.models.get('auth:User')
|
||||
@ -258,6 +304,7 @@ class AuthController extends Controller {
|
||||
}
|
||||
|
||||
await group.save()
|
||||
await group.get_gid_number()
|
||||
return res.api(await group.to_api())
|
||||
}
|
||||
|
||||
@ -317,6 +364,7 @@ class AuthController extends Controller {
|
||||
|
||||
await user.reset_password(req.body.password, 'create')
|
||||
await user.save()
|
||||
await user.grant_defaults()
|
||||
return res.api(await user.to_api())
|
||||
}
|
||||
|
||||
@ -365,7 +413,10 @@ class AuthController extends Controller {
|
||||
}
|
||||
|
||||
group.name = req.body.name
|
||||
group.grants_sudo = !!req.body.grants_sudo
|
||||
|
||||
await group.save()
|
||||
await group.get_gid_number()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ class IAMController extends Controller {
|
||||
.message(`${req.T('api.missing_field', true)} entity_id, target_id`)
|
||||
.api()
|
||||
|
||||
return res.api(await Policy.check_entity_access(req.body.entity_id, req.body.target_id))
|
||||
return res.api(await Policy.check_entity_access(req.body.entity_id, req.body.target_id, req.body.permission || undefined))
|
||||
}
|
||||
|
||||
async check_user_access(req, res, next) {
|
||||
@ -39,7 +39,7 @@ class IAMController extends Controller {
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
return res.api(await Policy.check_user_access(user, req.body.target_id))
|
||||
return res.api(await Policy.check_user_access(user, req.body.target_id, req.body.permission || undefined))
|
||||
}
|
||||
|
||||
async get_policies(req, res, next) {
|
||||
@ -56,6 +56,33 @@ class IAMController extends Controller {
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_permissions(req, res, next) {
|
||||
const Permission = this.models.get('iam:Permission')
|
||||
const permissions = await Permission.find({
|
||||
active: true,
|
||||
...(req.query.target_type ? {
|
||||
target_type: req.query.target_type,
|
||||
} : {})
|
||||
})
|
||||
const data = []
|
||||
|
||||
for ( const perm of permissions ) {
|
||||
if ( req.user.can(`iam:permission:${perm.target_type}:view`) ) {
|
||||
data.push(await perm.to_api())
|
||||
}
|
||||
}
|
||||
|
||||
if ( req.query.include_unset ) {
|
||||
data.reverse().push({
|
||||
permission: '',
|
||||
})
|
||||
|
||||
data.reverse()
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_policy(req, res, next) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const policy = await Policy.findById(req.params.id)
|
||||
@ -73,6 +100,23 @@ class IAMController extends Controller {
|
||||
return res.api(await policy.to_api())
|
||||
}
|
||||
|
||||
async get_permission(req, res, next) {
|
||||
const Permission = this.models.get('iam:Permission')
|
||||
const permission = await Permission.findById(req.params.id)
|
||||
|
||||
if ( !permission )
|
||||
return res.status(404)
|
||||
.message(req.T('iam.permission_not_found'))
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`iam:permission:${permission.target_type}:view`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
return res.api(await permission.to_api())
|
||||
}
|
||||
|
||||
async create_policy(req, res, next) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
|
||||
@ -108,12 +152,12 @@ class IAMController extends Controller {
|
||||
|
||||
if ( !['allow', 'deny'].includes(req.body.access_type) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('common.invalid')} access_type. ${req.T('api:must_one')} allow, deny.`)
|
||||
.message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`)
|
||||
.api()
|
||||
|
||||
if ( !['application', 'api_scope'].includes(req.body.target_type) )
|
||||
if ( !['application', 'api_scope', 'machine', 'machine_group'].includes(req.body.target_type) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('common.invalid')} target_type. ${req.T('api:must_one')} application, api_scope.`)
|
||||
.message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope, machine, machine_group.`)
|
||||
.api()
|
||||
|
||||
// Make sure the target_id is valid
|
||||
@ -130,6 +174,20 @@ class IAMController extends Controller {
|
||||
return res.status(400)
|
||||
.message(`${req.T('common.invalid')} target_id.`)
|
||||
.api()
|
||||
} else if ( req.body.target_type === 'machine' ) {
|
||||
const Machine = this.models.get('ldap:Machine')
|
||||
const machine = await Machine.findById(req.body.target_id)
|
||||
if ( !machine || !machine.active || !req.user.can(`ldap:machine:${machine.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('common.invalid')} target_id.`)
|
||||
.api()
|
||||
} else if ( req.body.target_type === 'machine_group' ) {
|
||||
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||
const group = await MachineGroup.findById(req.body.target_id)
|
||||
if ( !group || !group.active || !req.user.can(`ldap:machine_group:${group.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('common.invalid')} target_id.`)
|
||||
.api()
|
||||
}
|
||||
|
||||
const policy = new Policy({
|
||||
@ -140,12 +198,71 @@ class IAMController extends Controller {
|
||||
target_id: req.body.target_id,
|
||||
})
|
||||
|
||||
if ( req.body.permission ) {
|
||||
// Validate the permission and set it, if it is valid
|
||||
const Permission = this.models.get('iam:Permission')
|
||||
const permission = await Permission.findOne({
|
||||
active: true,
|
||||
target_type: req.body.target_type,
|
||||
permission: req.body.permission,
|
||||
})
|
||||
|
||||
if ( permission ) {
|
||||
policy.for_permission = true
|
||||
policy.permission = req.body.permission
|
||||
}
|
||||
}
|
||||
|
||||
await policy.save()
|
||||
req.user.allow(`iam:policy:${policy.id}`)
|
||||
await req.user.save()
|
||||
return res.api(await policy.to_api())
|
||||
}
|
||||
|
||||
async create_permission(req, res, next) {
|
||||
const Permission = this.models.get('iam:Permission')
|
||||
|
||||
const required_fields = ['target_type', 'permission']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.missing_field')} ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group']
|
||||
if ( !valid_target_types.includes(req.body.target_type) ) {
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.invalid_target_type')}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !req.user.can(`iam:permission${req.body.target_type}:create`) ) {
|
||||
return res.status(401).api()
|
||||
}
|
||||
|
||||
// Make sure one doesn't already exist
|
||||
const existing = await Permission.findOne({
|
||||
active: true,
|
||||
target_type: req.body.target_type,
|
||||
permission: req.body.permission,
|
||||
})
|
||||
|
||||
if ( existing ) {
|
||||
return res.status(400)
|
||||
.message(req.T('api.permission_already_exists'))
|
||||
.api()
|
||||
}
|
||||
|
||||
const perm = new Permission({
|
||||
target_type: req.body.target_type,
|
||||
permission: req.body.permission,
|
||||
})
|
||||
|
||||
await perm.save()
|
||||
return res.api(await perm.to_api())
|
||||
}
|
||||
|
||||
async update_policy(req, res, next) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const policy = await Policy.findById(req.params.id)
|
||||
@ -195,9 +312,9 @@ class IAMController extends Controller {
|
||||
.message(`${req.T('common.invalid')} access_type. ${req.T('api.must_one')} allow, deny.`)
|
||||
.api()
|
||||
|
||||
if ( !['application', 'api_scope'].includes(req.body.target_type) )
|
||||
if ( !['application', 'api_scope', 'machine', 'machine_group'].includes(req.body.target_type) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope.`)
|
||||
.message(`${req.T('common.invalid')} target_type. ${req.T('api.must_one')} application, api_scope, machine, machine_group.`)
|
||||
.api()
|
||||
|
||||
// Make sure the target_id is valid
|
||||
@ -214,6 +331,20 @@ class IAMController extends Controller {
|
||||
return res.status(400)
|
||||
.message(`${req.T('common.invalid')} target_id.`)
|
||||
.api()
|
||||
} else if ( req.body.target_type === 'machine' ) {
|
||||
const Machine = this.models.get('ldap:Machine')
|
||||
const machine = await Machine.findById(req.body.target_id)
|
||||
if ( !machine || !machine.active || !req.user.can(`ldap:machine:${machine.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('common.invalid')} target_id.`)
|
||||
.api()
|
||||
} else if ( req.body.target_type === 'machine_group' ) {
|
||||
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||
const group = await MachineGroup.findById(req.body.target_id)
|
||||
if ( !group || !group.active || !req.user.can(`ldap:machine_group:${group.id}:view`) )
|
||||
return res.status(400)
|
||||
.message(`${req.T('common.invalid')} target_id.`)
|
||||
.api()
|
||||
}
|
||||
|
||||
policy.entity_type = req.body.entity_type
|
||||
@ -221,10 +352,69 @@ class IAMController extends Controller {
|
||||
policy.access_type = req.body.access_type
|
||||
policy.target_type = req.body.target_type
|
||||
policy.target_id = req.body.target_id
|
||||
|
||||
if ( req.body.permission ) {
|
||||
// Validate the permission and set it, if it is valid
|
||||
const Permission = this.models.get('iam:Permission')
|
||||
const permission = await Permission.findOne({
|
||||
active: true,
|
||||
target_type: req.body.target_type,
|
||||
permission: req.body.permission,
|
||||
})
|
||||
|
||||
if ( permission ) {
|
||||
policy.for_permission = true
|
||||
policy.permission = req.body.permission
|
||||
} else {
|
||||
policy.for_permission = false
|
||||
policy.permission = undefined
|
||||
}
|
||||
} else {
|
||||
policy.for_permission = false
|
||||
policy.permission = undefined
|
||||
}
|
||||
|
||||
await policy.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async update_permission(req, res, next) {
|
||||
const Permission = this.models.get('iam:Permission')
|
||||
|
||||
const required_fields = ['target_type', 'permission']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.missing_field')} ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
const valid_target_types = ['application', 'api_scope', 'machine', 'machine_group']
|
||||
if ( !valid_target_types.includes(req.body.target_type) ) {
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.invalid_target_type')}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !req.user.can(`iam:permission${req.body.target_type}:update`) ) {
|
||||
return res.status(401).api()
|
||||
}
|
||||
|
||||
// Make sure one doesn't already exist
|
||||
const existing = await Permission.findById(req.params.id)
|
||||
if ( !existing?.active ) {
|
||||
return res.status(404)
|
||||
.message(req.T('api.permission_not_found'))
|
||||
.api()
|
||||
}
|
||||
|
||||
existing.target_type = req.body.target_type
|
||||
existing.permission = req.body.permission
|
||||
|
||||
await existing.save()
|
||||
return res.api(await existing.to_api())
|
||||
}
|
||||
|
||||
async delete_policy(req, res, next) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const policy = await Policy.findById(req.params.id)
|
||||
@ -243,6 +433,27 @@ class IAMController extends Controller {
|
||||
await policy.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_permission(req, res, next) {
|
||||
const Permission = this.models.get('iam:Permission')
|
||||
const permission = await Permission.findById(req.params.id)
|
||||
|
||||
if ( !permission?.active ) {
|
||||
return res.status(404)
|
||||
.message(req.T('api.permission_not_found'))
|
||||
.api()
|
||||
}
|
||||
|
||||
if ( !req.user.can(`iam:permission:${permission.target_type}:delete`) ) {
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
}
|
||||
|
||||
permission.active = false
|
||||
await permission.save()
|
||||
return res.api()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = IAMController
|
||||
|
@ -46,6 +46,32 @@ class LDAPController extends Controller {
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_machines(req, res, next) {
|
||||
const Machine = this.models.get('ldap:Machine')
|
||||
const machines = await Machine.find({active: true})
|
||||
const data = []
|
||||
|
||||
for ( const machine of machines ) {
|
||||
if ( !req.user.can(`ldap:machine:${machine.id}:view`) ) continue
|
||||
data.push(await machine.to_api())
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_machine_groups(req, res, next) {
|
||||
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||
const groups = await MachineGroup.find({active: true})
|
||||
const data = []
|
||||
|
||||
for ( const group of groups ) {
|
||||
if ( !req.user.can(`ldap:machine_group:${group.id}:view`) ) continue
|
||||
data.push(await group.to_api())
|
||||
}
|
||||
|
||||
return res.api(data)
|
||||
}
|
||||
|
||||
async get_client(req, res, next) {
|
||||
const Client = this.models.get('ldap:Client')
|
||||
const client = await Client.findById(req.params.id)
|
||||
@ -80,6 +106,40 @@ class LDAPController extends Controller {
|
||||
return res.api(await group.to_api())
|
||||
}
|
||||
|
||||
async get_machine(req, res, next) {
|
||||
const Machine = this.models.get('ldap:Machine')
|
||||
const machine = await Machine.findById(req.params.id)
|
||||
|
||||
if ( !machine || !machine.active )
|
||||
return res.status(404)
|
||||
.message(req.T('api.machine_not_found'))
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`ldap:machine:${machine.id}:view`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
return res.api(await machine.to_api())
|
||||
}
|
||||
|
||||
async get_machine_group(req, res, next) {
|
||||
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||
const group = await MachineGroup.findById(req.params.id)
|
||||
|
||||
if ( !group || !group.active )
|
||||
return res.status(404)
|
||||
.message(req.T('api.group_not_found'))
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`ldap:machine_group:${group.id}:view`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
return res.api(await group.to_api())
|
||||
}
|
||||
|
||||
async create_client(req, res, next) {
|
||||
if ( !req.user.can('ldap:client:create') )
|
||||
return res.status(401)
|
||||
@ -121,13 +181,89 @@ class LDAPController extends Controller {
|
||||
return res.api(await client.to_api())
|
||||
}
|
||||
|
||||
async create_group(req, res, next) {
|
||||
console.log(req.body)
|
||||
if ( !req.user.can(`ldap:group:create`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
async create_machine(req, res, next) {
|
||||
// validate inputs
|
||||
const required_fields = ['name', 'description']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.missing_field')} ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Make sure the machine name is free
|
||||
const Machine = this.models.get('ldap:Machine')
|
||||
const existing_machine = await Machine.findOne({ name: req.body.name })
|
||||
if ( existing_machine )
|
||||
return res.status(400)
|
||||
.message(req.T('api.machine_already_exists'))
|
||||
.api()
|
||||
|
||||
const machine = new Machine({
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
host_name: req.body.host_name,
|
||||
location: req.body.location,
|
||||
})
|
||||
|
||||
if ( req.body.bind_password ) {
|
||||
await machine.set_bind_password(req.body.bind_password)
|
||||
}
|
||||
|
||||
if ( 'ldap_visible' in req.body ) {
|
||||
machine.ldap_visible = !!req.body.ldap_visible
|
||||
}
|
||||
|
||||
await machine.save()
|
||||
return res.api(await machine.to_api())
|
||||
}
|
||||
|
||||
async create_machine_group(req, res, next) {
|
||||
// validate inputs
|
||||
const required_fields = ['name']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.missing_field')} ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Make sure the machine name is free
|
||||
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||
const existing_group = await MachineGroup.findOne({ name: req.body.name })
|
||||
if ( existing_group )
|
||||
return res.status(400)
|
||||
.message(req.T('api.group_already_exists'))
|
||||
.api()
|
||||
|
||||
const group = new MachineGroup({
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
})
|
||||
|
||||
if ( 'ldap_visible' in req.body ) {
|
||||
group.ldap_visible = !!req.body.ldap_visible
|
||||
}
|
||||
|
||||
const Machine = this.models.get('ldap:Machine')
|
||||
const machine_ids = Array.isArray(req.body.machine_ids) ? req.body.machine_ids : []
|
||||
group.machine_ids = []
|
||||
for ( const potential of machine_ids ) {
|
||||
const machine = await Machine.findOne({
|
||||
_id: Machine.to_object_id(potential),
|
||||
active: true,
|
||||
})
|
||||
|
||||
if ( machine ) {
|
||||
group.machine_ids.push(potential)
|
||||
}
|
||||
}
|
||||
|
||||
await group.save()
|
||||
return res.api(await group.to_api())
|
||||
}
|
||||
|
||||
async create_group(req, res, next) {
|
||||
// validate inputs
|
||||
const required_fields = ['role', 'name']
|
||||
for ( const field of required_fields ) {
|
||||
@ -240,6 +376,106 @@ class LDAPController extends Controller {
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async update_machine(req, res, next) {
|
||||
const Machine = this.models.get('ldap:Machine')
|
||||
|
||||
const machine = await Machine.findById(req.params.id)
|
||||
if ( !machine || !machine.active )
|
||||
return res.status(404)
|
||||
.message(req.T('api.machine_not_found'))
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`ldap:machine:${machine.id}:update`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
const required_fields = ['name', 'description']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.missing_field')} ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Make sure the machine name is free
|
||||
const existing_machine = await Machine.findOne({ name: req.body.name })
|
||||
if ( existing_machine && existing_machine.id !== machine.id )
|
||||
return res.status(400)
|
||||
.message(req.T('api.machine_already_exists'))
|
||||
.api()
|
||||
|
||||
machine.name = req.body.name
|
||||
machine.description = req.body.description
|
||||
machine.host_name = req.body.host_name
|
||||
machine.location = req.body.location
|
||||
|
||||
if ( req.body.bind_password ) {
|
||||
await machine.set_bind_password(req.body.bind_password)
|
||||
}
|
||||
|
||||
if ( 'ldap_visible' in req.body ) {
|
||||
machine.ldap_visible = !!req.body.ldap_visible
|
||||
}
|
||||
|
||||
await machine.save()
|
||||
return res.api(await machine.to_api())
|
||||
}
|
||||
|
||||
async update_machine_group(req, res, next) {
|
||||
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||
|
||||
const group = await MachineGroup.findById(req.params.id)
|
||||
if ( !group || !group.active )
|
||||
return res.status(404)
|
||||
.message(req.T('api.group_not_found'))
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`ldap:machine_group:${group.id}:update`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
const required_fields = ['name']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field] )
|
||||
return res.status(400)
|
||||
.message(`${req.T('api.missing_field')} ${field}`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Make sure the machine name is free
|
||||
const existing_group = await MachineGroup.findOne({ name: req.body.name })
|
||||
if ( existing_group && existing_group.id !== group.id )
|
||||
return res.status(400)
|
||||
.message(req.T('api.group_already_exists'))
|
||||
.api()
|
||||
|
||||
group.name = req.body.name
|
||||
group.description = req.body.description
|
||||
|
||||
if ( 'ldap_visible' in req.body ) {
|
||||
group.ldap_visible = !!req.body.ldap_visible
|
||||
}
|
||||
|
||||
const Machine = this.models.get('ldap:Machine')
|
||||
const machine_ids = Array.isArray(req.body.machine_ids) ? req.body.machine_ids : []
|
||||
group.machine_ids = []
|
||||
for ( const potential of machine_ids ) {
|
||||
const machine = await Machine.findOne({
|
||||
_id: Machine.to_object_id(potential),
|
||||
active: true,
|
||||
})
|
||||
|
||||
if ( machine ) {
|
||||
group.machine_ids.push(potential)
|
||||
}
|
||||
}
|
||||
|
||||
await group.save()
|
||||
return res.api(await group.to_api())
|
||||
}
|
||||
|
||||
async update_group(req, res, next) {
|
||||
const User = await this.models.get('auth:User')
|
||||
const Group = await this.models.get('ldap:Group')
|
||||
@ -337,6 +573,44 @@ class LDAPController extends Controller {
|
||||
await group.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_machine(req, res, next) {
|
||||
const Machine = this.models.get('ldap:Machine')
|
||||
const machine = await Machine.findById(req.params.id)
|
||||
|
||||
if ( !machine || !machine.active )
|
||||
return res.status(404)
|
||||
.message(req.T('api.machine_not_found'))
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`ldap:machine:${machine.id}:delete`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
machine.active = false
|
||||
await machine.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async delete_machine_group(req, res, next) {
|
||||
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||
const group = await MachineGroup.findById(req.params.id)
|
||||
|
||||
if ( !group || !group.active )
|
||||
return res.status(404)
|
||||
.message(req.T('api.group_not_found'))
|
||||
.api()
|
||||
|
||||
if ( !req.user.can(`ldap:machine_group:${group.id}:delete`) )
|
||||
return res.status(401)
|
||||
.message(req.T('api.insufficient_permissions'))
|
||||
.api()
|
||||
|
||||
group.active = false
|
||||
await group.save()
|
||||
return res.api()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = LDAPController
|
||||
|
@ -20,6 +20,7 @@ class PasswordController extends Controller {
|
||||
return {
|
||||
created: x.created,
|
||||
expires: x.expires,
|
||||
accessed: x.accessed,
|
||||
active: x.active,
|
||||
name: x.name ?? req.T('common.unnamed'),
|
||||
uuid: x.uuid,
|
||||
@ -90,6 +91,10 @@ class PasswordController extends Controller {
|
||||
await this.activity.password_reset({ req, ip: req.ip })
|
||||
if ( req.trap.has_trap() && req.trap.get_trap() === 'password_reset' ) await req.trap.end()
|
||||
|
||||
if ( req.session.registrant_flow ) {
|
||||
await req.trap.begin('registrant_flow', { session_only: true })
|
||||
}
|
||||
|
||||
// invalidate existing tokens and other logins
|
||||
await req.user.logout(req)
|
||||
await req.user.kickout()
|
||||
|
@ -24,8 +24,9 @@ class ProfileController extends Controller {
|
||||
last_name: user.last_name,
|
||||
email: user.email,
|
||||
uid: user.uid,
|
||||
tagline: user.tagline,
|
||||
tagline: user.tagline || '',
|
||||
user_id: user.id,
|
||||
login_shell: user.login_shell || '',
|
||||
...(user.notify_config ? { notify_config: await user.notify_config.to_api() } : {})
|
||||
})
|
||||
}
|
||||
@ -123,6 +124,8 @@ class ProfileController extends Controller {
|
||||
|
||||
async update(req, res, next) {
|
||||
const User = this.models.get('auth:User')
|
||||
const Message = this.models.get('Message')
|
||||
const Setting = this.models.get('Setting')
|
||||
|
||||
let user
|
||||
if ( req.params.user_id === 'me' ) user = req.user
|
||||
@ -154,14 +157,22 @@ class ProfileController extends Controller {
|
||||
.api()
|
||||
|
||||
// Update the user's profile
|
||||
if ( user.email !== req.body.email && (await Setting.get('auth.require_email_verify')) ) {
|
||||
await req.trap.begin('verify_email', { session_only: false })
|
||||
await Message.create(req.user, 'Your e-mail address has changed, and a verification e-mail has been sent. You must complete this process to continue.')
|
||||
}
|
||||
|
||||
user.first_name = req.body.first_name
|
||||
user.last_name = req.body.last_name
|
||||
user.email = req.body.email
|
||||
user.tagline = req.body.tagline
|
||||
user.login_shell = req.body.login_shell
|
||||
|
||||
// Save the record
|
||||
await user.save()
|
||||
return res.api()
|
||||
return res.api({
|
||||
force_message_refresh: true,
|
||||
})
|
||||
}
|
||||
|
||||
async update_photo(req, res, next) {
|
||||
|
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 {
|
||||
static get services() {
|
||||
return [...super.services, 'Vue', 'models']
|
||||
return [...super.services, 'Vue', 'models', 'jobs']
|
||||
}
|
||||
|
||||
async registration_provider_get(req, res, next) {
|
||||
if ( req.session.auth.flow ) {
|
||||
req.session.registrant_flow = req.session.auth.flow
|
||||
}
|
||||
|
||||
return res.page('auth:register', {
|
||||
...this.Vue.data({})
|
||||
})
|
||||
}
|
||||
|
||||
async email_verify_keyaction(req, res, next) {
|
||||
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
|
||||
req.user.email_verified = true
|
||||
await req.user.save()
|
||||
await req.trap.end()
|
||||
const url = req.session.email_verify_flow || '/dash/profile'
|
||||
return res.redirect(url)
|
||||
}
|
||||
|
||||
async show_verify_email(req, res, next) {
|
||||
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
|
||||
const verify_queue = this.jobs.queue('verifications')
|
||||
await verify_queue.add('SendVerificationEmail', { user_id: req.user.id })
|
||||
|
||||
return res.page('public:message', {
|
||||
...this.Vue.data({
|
||||
message: req.T('auth.must_verify_email'),
|
||||
actions: [
|
||||
{
|
||||
text: 'Send Verification E-Mail',
|
||||
action: 'redirect',
|
||||
next: '/auth/verify-email/sent',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async send_verify_email(req, res, next) {
|
||||
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
|
||||
return res.page('public:message', {
|
||||
...this.Vue.data({
|
||||
message: req.T('auth.verify_email_sent'),
|
||||
actions: [
|
||||
{
|
||||
text: 'Re-send Verification E-Mail',
|
||||
action: 'redirect',
|
||||
next: '/auth/verify-email/sent',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async finish_registration(req, res, next) {
|
||||
if ( req.trap.has_trap() && req.trap.get_trap() === 'registrant_flow' ) await req.trap.end()
|
||||
const dest = req.session.registrant_flow || '/dash/profile'
|
||||
return res.redirect(dest)
|
||||
}
|
||||
|
||||
async login_provider_get(req, res, next) {
|
||||
const Setting = this.models.get('Setting')
|
||||
|
||||
|
@ -23,13 +23,13 @@ class Oauth2 extends Oauth2Controller {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const application = await Application.findOne({ oauth_client_ids: starship_client.id })
|
||||
if ( !application ) {
|
||||
this.output.warn('IAM Denial!')
|
||||
this.output.warn(`IAM Denial: OAuth client not associated with an application: ${starship_client.id}`)
|
||||
return this.Vue.auth_message(res, {
|
||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||
next_destination: '/dash',
|
||||
})
|
||||
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
|
||||
this.output.warn('IAM Denial!')
|
||||
this.output.warn(`IAM Denial: User ${req.user.uid} not authorized to access application: ${application.id}`)
|
||||
return this.Vue.auth_message(res, {
|
||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||
next_destination: '/dash',
|
||||
@ -44,7 +44,7 @@ class Oauth2 extends Oauth2Controller {
|
||||
async authorize_get(req, res, next) {
|
||||
const client = await this._get_authorize_client(req)
|
||||
if ( !client ) return this._uniform(res, req.T('auth.unable_to_authorize'))
|
||||
const uri = new URL(req.query.redirect_uri)
|
||||
const uri = new URL(Array.isArray(req.query.redirect_uri) ? req.query.redirect_uri[0] : req.query.redirect_uri)
|
||||
|
||||
const StarshipClient = this.models.get('oauth:Client')
|
||||
const starship_client = await StarshipClient.findOne({ active: true, uuid: client.clientID })
|
||||
@ -54,19 +54,25 @@ class Oauth2 extends Oauth2Controller {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const application = await Application.findOne({ oauth_client_ids: starship_client.id })
|
||||
if ( !application ) {
|
||||
this.output.warn('IAM Denial!')
|
||||
this.output.warn(`IAM Denial: OAuth client not associated with an application: ${starship_client.id}`)
|
||||
return this.Vue.auth_message(res, {
|
||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||
next_destination: '/dash',
|
||||
})
|
||||
} else if ( !(await Policy.check_user_access(req.user, application.id)) ) {
|
||||
this.output.warn('IAM Denial!')
|
||||
this.output.warn(`IAM Denial: User ${req.user.uid} not authorized to access application: ${application.id}`)
|
||||
return this.Vue.auth_message(res, {
|
||||
message: req.T('saml.no_access').replace('APP_NAME', application.name),
|
||||
next_destination: '/dash',
|
||||
})
|
||||
}
|
||||
|
||||
let state;
|
||||
if ( state = (req.query.state || req.body.state) ) {
|
||||
state = Array.isArray(state) ? state[0] : state
|
||||
uri.searchParams.set('state', state)
|
||||
}
|
||||
|
||||
if ( req.user.has_authorized(starship_client) ) {
|
||||
return this.Vue.invoke_action(res, {
|
||||
text: 'Grant Access',
|
||||
|
@ -12,7 +12,7 @@ class EMailJob extends Job {
|
||||
|
||||
const { data } = job
|
||||
let { from = config.default_sender, to, subject, html = undefined, email_params = undefined } = data
|
||||
this.output.info(`Sending mail to ${to}...`)
|
||||
this.info(`Sending mail to ${to}...`)
|
||||
|
||||
if ( !html && email_params ) html = this.email(email_params)
|
||||
|
||||
@ -20,9 +20,11 @@ class EMailJob extends Job {
|
||||
from, to, subject, html,
|
||||
})
|
||||
} catch (e) {
|
||||
this.output.error(e)
|
||||
this.error(e)
|
||||
throw e
|
||||
}
|
||||
this.output.success(`Mail sent!`)
|
||||
|
||||
this.success(`Mail sent!`)
|
||||
}
|
||||
|
||||
email({ header_text, body_paragraphs = [], button_text = '', button_link = '' }) {
|
||||
|
@ -12,7 +12,7 @@ class ForeignIPLoginAlertJob extends Job {
|
||||
const user = await User.findById(user_id)
|
||||
if ( !user ) throw new Error('Unable to find user with ID: '+user_id)
|
||||
|
||||
this.output.info('Sending foreign IP login alert to user.')
|
||||
this.info('Sending foreign IP login alert to user ' + user.uid)
|
||||
|
||||
await this.jobs.queue('mailer').add('EMail', {
|
||||
to: user.email,
|
||||
@ -29,14 +29,19 @@ class ForeignIPLoginAlertJob extends Job {
|
||||
}
|
||||
})
|
||||
|
||||
this.info('Logged e-mail job')
|
||||
|
||||
if ( user.notify_config && user.notify_config.active ) {
|
||||
await user.notify_config.log({
|
||||
title: `${this.configs.get('app.name')}: Sign-In From New IP`,
|
||||
message: `Someone signed into your account (${user.uid}) from the IP address ${ip}. If this was you, no further action is required.`,
|
||||
})
|
||||
|
||||
this.info('Logged push notification job')
|
||||
}
|
||||
} catch (e) {
|
||||
this.output.error(e)
|
||||
this.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,13 +13,17 @@ class PasswordResetJob extends Job {
|
||||
const User = this.models.get('auth:User')
|
||||
const user = await User.findById(user_id)
|
||||
if (!user) {
|
||||
this.output.error(`Unable to find user with ID: ${user_id}`)
|
||||
this.error(`Unable to find user with ID: ${user_id}`)
|
||||
throw new Error('Unable to find user with that ID.')
|
||||
}
|
||||
|
||||
this.output.info(`Resetting password for user: ${user.uid}`)
|
||||
this.info(`Resetting password for user: ${user.uid}`)
|
||||
|
||||
// Create an authenticated key-action
|
||||
const key_action = await this.key_action(user)
|
||||
|
||||
this.info(`Created reset keyaction ${key_action.id} (key: ${key_action.key}, handler: ${key_action.handler})`)
|
||||
|
||||
await this.jobs.queue('mailer').add('EMail', {
|
||||
to: user.email,
|
||||
subject: 'Reset Your Password | ' + this.configs.get('app.name'),
|
||||
@ -34,17 +38,22 @@ class PasswordResetJob extends Job {
|
||||
}
|
||||
})
|
||||
|
||||
this.info('Logged e-mail job.')
|
||||
|
||||
if ( user.notify_config && user.notify_config.active ) {
|
||||
await user.notify_config.log({
|
||||
title: `${this.configs.get('app.name')}: Password Reset Requested`,
|
||||
message: `A password reset request was logged for your account (${user.uid}). If this was you, please check your e-mail for further instructions.`,
|
||||
priority: 8,
|
||||
})
|
||||
|
||||
this.info('Logged security push notification job')
|
||||
}
|
||||
|
||||
this.output.success('Password reset logged.')
|
||||
this.success('Password reset logged.')
|
||||
} catch (e) {
|
||||
this.output.error(e)
|
||||
this.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ class PasswordResetAlertJob extends Job {
|
||||
const user = await User.findById(user_id)
|
||||
if ( !user ) throw new Error('Unable to find user with ID: '+user_id)
|
||||
|
||||
this.output.info('Sending password reset alert to user.')
|
||||
this.info('Sending password reset alert to user ' + user.uid)
|
||||
|
||||
await this.jobs.queue('mailer').add('EMail', {
|
||||
to: user.email,
|
||||
@ -28,15 +28,20 @@ class PasswordResetAlertJob extends Job {
|
||||
},
|
||||
})
|
||||
|
||||
this.info('Logged e-mail job')
|
||||
|
||||
if ( user.notify_config && user.notify_config.active ) {
|
||||
await user.notify_config.log({
|
||||
title: `${this.configs.get('app.name')}: Password Reset`,
|
||||
message: `The password to your account (${user.uid}) was reset from the IP address ${ip}. If this was not you, please contact your system administrator.`,
|
||||
priority: 8,
|
||||
})
|
||||
|
||||
this.info('Logged push notification job')
|
||||
}
|
||||
} catch (e) {
|
||||
this.output.error(e)
|
||||
this.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,14 +14,15 @@ class PopulateAnnouncementJob extends Job {
|
||||
const announcement = await Announcement.findById(announcement_id)
|
||||
|
||||
if ( !announcement ) {
|
||||
this.output.error(`Unable to find announcement with ID: ${announcement_id}`)
|
||||
this.error(`Unable to find announcement with ID: ${announcement_id}`)
|
||||
throw new Error('Unable to find announcement with that ID.')
|
||||
}
|
||||
|
||||
await announcement.populate()
|
||||
this.output.success('Populated announcements.')
|
||||
this.success('Populated announcements.')
|
||||
} catch (e) {
|
||||
this.output.error(e)
|
||||
this.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,13 +18,15 @@ class PushNotifyJob extends Job {
|
||||
const notify = user.notify_config
|
||||
if ( !notify || !notify.active ) throw new Error('User does not have notifications configured.')
|
||||
|
||||
this.output.info(`Sending notification to ${user.uid}...`)
|
||||
this.info(`Sending notification to ${user.uid}...`)
|
||||
|
||||
await notify.send({ title, message, priority })
|
||||
} catch (e) {
|
||||
this.output.error(e)
|
||||
this.error(e)
|
||||
throw e
|
||||
}
|
||||
this.output.success(`Notification sent!`)
|
||||
|
||||
this.success(`Notification sent!`)
|
||||
}
|
||||
}
|
||||
|
||||
|
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 LDAP = require('ldapjs')
|
||||
|
||||
class GroupsController extends LDAPController {
|
||||
static get services() {
|
||||
|
@ -59,34 +59,8 @@ class LDAPController extends Injectable {
|
||||
return next(new LDAP.InsufficientAccessRightsError())
|
||||
}
|
||||
|
||||
// Check if the credentials are an app_password
|
||||
const app_password_verified = Array.isArray(item.app_passwords)
|
||||
&& item.app_passwords.length > 0
|
||||
&& await item.check_app_password(req.credentials)
|
||||
|
||||
// Check if the user has MFA enabled.
|
||||
// If so, split the incoming password to fetch the MFA code
|
||||
// e.g. normalPassword:123456
|
||||
if ( !app_password_verified && item.mfa_enabled ) {
|
||||
const parts = req.credentials.split(':')
|
||||
const mfa_code = parts.pop()
|
||||
const actual_password = parts.join(':')
|
||||
|
||||
// Check the credentials
|
||||
if ( !await item.check_password(actual_password) ) {
|
||||
this.output.debug(`Bind failure: user w/ MFA provided invalid credentials`)
|
||||
return next(new LDAP.InvalidCredentialsError('Invalid credentials. Make sure MFA code is included at the end of your password (e.g. password:123456)'))
|
||||
}
|
||||
|
||||
// Now, check the MFA code
|
||||
if ( !item.mfa_token.verify(mfa_code) ) {
|
||||
this.output.debug(`Bind failure: user w/ MFA provided invalid MFA token`)
|
||||
return next(new LDAP.InvalidCredentialsError('Invalid credentials. Verification of the MFA token failed.'))
|
||||
}
|
||||
|
||||
// If not MFA, just check the credentials
|
||||
} else if (!app_password_verified && !await item.check_password(req.credentials)) {
|
||||
this.output.debug(`Bind failure: user w/ simple auth provided invalid credentials`)
|
||||
// Check if the credentials are valid
|
||||
if ( !(await item.check_credential_string(req.credentials)) ) {
|
||||
return next(new LDAP.InvalidCredentialsError())
|
||||
}
|
||||
|
||||
|
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],
|
||||
oauth_client_ids: [String],
|
||||
openid_client_ids: [String],
|
||||
radius_client_ids: [String],
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +25,7 @@ class ApplicationModel extends Model {
|
||||
ldap_client_ids: this.ldap_client_ids,
|
||||
oauth_client_ids: this.oauth_client_ids,
|
||||
openid_client_ids: this.openid_client_ids,
|
||||
radius_client_ids: this.radius_client_ids || [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ class AppPasswordModel extends Model {
|
||||
return {
|
||||
hash: String,
|
||||
created: { type: Date, default: () => new Date },
|
||||
accessed: Date,
|
||||
expires: Date,
|
||||
active: { type: Boolean, default: true },
|
||||
name: String,
|
||||
|
@ -11,6 +11,9 @@ class GroupModel extends Model {
|
||||
return {
|
||||
name: String,
|
||||
user_ids: [String],
|
||||
posix_user_id: String,
|
||||
posix_group_id: Number,
|
||||
grants_sudo: { type: Boolean, default: false },
|
||||
active: { type: Boolean, default: true },
|
||||
ldap_visible: { type: Boolean, default: true },
|
||||
}
|
||||
@ -29,18 +32,72 @@ class GroupModel extends Model {
|
||||
return await User.find({ _id: { $in: this.user_ids.map(x => this.constructor.to_object_id(x)) } })
|
||||
}
|
||||
|
||||
async get_gid_number() {
|
||||
if ( !this.posix_group_id ) {
|
||||
const Setting = this.models.get('Setting')
|
||||
let last_uid = await Setting.get('ldap.last_alloc_uid')
|
||||
if ( last_uid < 1 ) {
|
||||
last_uid = this.configs.get('ldap:server.schema.start_uid')
|
||||
}
|
||||
|
||||
this.posix_group_id = last_uid + 1
|
||||
await Setting.set('ldap.last_alloc_uid', this.posix_group_id)
|
||||
await this.save()
|
||||
}
|
||||
|
||||
return this.posix_group_id
|
||||
}
|
||||
|
||||
async to_ldap() {
|
||||
const users = await this.users()
|
||||
return {
|
||||
cn: this.name,
|
||||
dn: this.dn.format(this.configs.get('ldap:server.format')),
|
||||
objectClass: 'groupOfNames',
|
||||
objectClass: ['groupOfNames', 'posixGroup'],
|
||||
gidNumber: String(await this.get_gid_number()),
|
||||
member: users.map(x => x.dn.format(this.configs.get('ldap:server.format'))),
|
||||
}
|
||||
}
|
||||
|
||||
static async sudo_directory() {
|
||||
const groups = await this.find({ ldap_visible: true, active: true, grants_sudo: true })
|
||||
|
||||
let users = []
|
||||
for ( const group of groups ) {
|
||||
users = [...users, ...(await group.users())]
|
||||
}
|
||||
|
||||
return users.filter(u => u.uid !== 'root')
|
||||
}
|
||||
|
||||
static async ldap_directory() {
|
||||
return this.find({ ldap_visible: true, active: true })
|
||||
const User = this.prototype.models.get('auth:User')
|
||||
const groups = await this.find({ ldap_visible: true, active: true })
|
||||
|
||||
const posix_user_ids = groups.map(group => group.posix_user_id)
|
||||
.filter(Boolean)
|
||||
.map(id => User.to_object_id(id))
|
||||
|
||||
const missing_posix_users = await User.find({
|
||||
ldap_visible: true,
|
||||
_id: {
|
||||
$nin: posix_user_ids
|
||||
}
|
||||
})
|
||||
|
||||
for ( const user of missing_posix_users ) {
|
||||
const group = new this({
|
||||
name: `${user.uid} (posix)`,
|
||||
user_ids: [user.id],
|
||||
posix_user_id: user.id,
|
||||
posix_group_id: await user.get_uid_number(),
|
||||
})
|
||||
|
||||
await group.save()
|
||||
groups.push(group)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
async to_api() {
|
||||
@ -49,6 +106,7 @@ class GroupModel extends Model {
|
||||
name: this.name,
|
||||
user_ids: this.user_ids,
|
||||
ldap_visible: this.ldap_visible,
|
||||
grants_sudo: !!this.grants_sudo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ class User extends AuthUser {
|
||||
last_name: String,
|
||||
tagline: String,
|
||||
email: String,
|
||||
email_verified: {type: Boolean, default: false},
|
||||
ldap_visible: {type: Boolean, default: true},
|
||||
active: {type: Boolean, default: true},
|
||||
mfa_token: MFAToken,
|
||||
@ -38,9 +39,42 @@ class User extends AuthUser {
|
||||
photo_file_id: String,
|
||||
trap: String,
|
||||
notify_config: NotifyConfig,
|
||||
uid_number: Number,
|
||||
login_shell: String,
|
||||
|
||||
is_default_user_for_coreid: { type: Boolean, default: false },
|
||||
}}
|
||||
}
|
||||
|
||||
async grant_defaults() {
|
||||
const default_user = await this.constructor.findOne({is_default_user_for_coreid: true, active: true})
|
||||
this.login_shell = default_user.login_shell
|
||||
this.roles = default_user.roles
|
||||
this.permissions = default_user.permissions
|
||||
|
||||
const groups = await default_user.groups()
|
||||
for ( const group of groups ) {
|
||||
group.user_ids.push(this.id)
|
||||
await group.save()
|
||||
}
|
||||
}
|
||||
|
||||
async get_uid_number() {
|
||||
if ( !this.uid_number ) {
|
||||
const Setting = this.models.get('Setting')
|
||||
let last_uid = await Setting.get('ldap.last_alloc_uid')
|
||||
if ( last_uid < 1 ) {
|
||||
last_uid = this.configs.get('ldap:server.schema.start_uid')
|
||||
}
|
||||
|
||||
this.uid_number = last_uid + 1
|
||||
await Setting.set('ldap.last_alloc_uid', this.uid_number)
|
||||
await this.save()
|
||||
}
|
||||
|
||||
return this.uid_number
|
||||
}
|
||||
|
||||
async photo() {
|
||||
const File = this.models.get('upload::File')
|
||||
return File.findById(this.photo_file_id)
|
||||
@ -77,10 +111,12 @@ class User extends AuthUser {
|
||||
uid: this.uid,
|
||||
first_name: this.first_name,
|
||||
last_name: this.last_name,
|
||||
name: `${this.first_name} ${this.last_name}`,
|
||||
email: this.email,
|
||||
tagline: this.tagline,
|
||||
trap: this.trap,
|
||||
group_ids: (await this.groups()).map(x => x.id),
|
||||
profile_photo: `${this.configs.get('app.url')}api/v1/auth/users/${this.uid}/photo`,
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,13 +155,49 @@ class User extends AuthUser {
|
||||
await this.save()
|
||||
}
|
||||
|
||||
async check_credential_string(credential) {
|
||||
// Check if the credentials are an app_password
|
||||
const app_password_verified = Array.isArray(this.app_passwords)
|
||||
&& this.app_passwords.length > 0
|
||||
&& await this.check_app_password(credential)
|
||||
|
||||
// Check if the user has MFA enabled.
|
||||
// If so, split the incoming password to fetch the MFA code
|
||||
// e.g. normalPassword:123456
|
||||
if ( !app_password_verified && this.mfa_enabled ) {
|
||||
const parts = credential.split(':')
|
||||
const mfa_code = parts.pop()
|
||||
const actual_password = parts.join(':')
|
||||
|
||||
// Check the credentials
|
||||
if ( !await this.check_password(actual_password) ) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Now, check the MFA code
|
||||
if ( !this.mfa_token.verify(mfa_code) ) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If not MFA, just check the credentials
|
||||
} else if (!app_password_verified && !await this.check_password(credential)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async check_password(password) {
|
||||
return this.get_provider().check_user_auth(this, password)
|
||||
}
|
||||
|
||||
async check_app_password(password) {
|
||||
for ( const pw of this.app_passwords ) {
|
||||
if ( await pw.verify(password) ) return true
|
||||
if ( await pw.verify(password) ) {
|
||||
pw.accessed = new Date
|
||||
await pw.save()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
@ -169,9 +241,45 @@ class User extends AuthUser {
|
||||
this.get_provider().logout(request)
|
||||
}
|
||||
|
||||
async has_sudo() {
|
||||
const groups = await this.groups()
|
||||
return groups.some(group => group.grants_sudo)
|
||||
}
|
||||
|
||||
async to_sudo(iam_targets = []) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
const granted = []
|
||||
for ( const target of iam_targets ) {
|
||||
if ( await Policy.check_user_access(this, target, 'sudo') ) {
|
||||
granted.push(target)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
objectClass: ['sudoRole'],
|
||||
cn: `sudo_${this.uid.toLowerCase()}`,
|
||||
sudoUser: this.uid.toLowerCase(),
|
||||
...(granted.length ? {
|
||||
iamtarget: granted,
|
||||
sudoHost: 'ALL',
|
||||
sudoRunAs: 'ALL',
|
||||
sudoCommand: 'ALL',
|
||||
} : {})
|
||||
}
|
||||
}
|
||||
|
||||
async to_ldap(iam_targets = []) {
|
||||
const Policy = this.models.get('iam:Policy')
|
||||
|
||||
const uid_number = await this.get_uid_number()
|
||||
const shell = this.login_shell || this.configs.get('ldap:server.schema.default_shell')
|
||||
const domain = this.configs.get('ldap:server.schema.base_dc').split(',').map(x => x.replace('dc=', '')).join('.')
|
||||
|
||||
const group_ids = []
|
||||
for ( const group of await this.groups() ) {
|
||||
group_ids.push(await group.get_gid_number())
|
||||
}
|
||||
|
||||
const ldap_data = {
|
||||
uid: this.uid.toLowerCase(),
|
||||
uuid: this.uuid,
|
||||
@ -179,10 +287,16 @@ class User extends AuthUser {
|
||||
sn: this.last_name,
|
||||
gecos: `${this.first_name} ${this.last_name}`,
|
||||
mail: this.email,
|
||||
objectClass: ['inetOrgPerson', 'person'],
|
||||
objectClass: ['inetOrgPerson', 'person', 'posixaccount'],
|
||||
objectclass: ['inetOrgPerson', 'person', 'posixaccount'],
|
||||
entryuuid: this.uuid,
|
||||
entryUUID: this.uuid,
|
||||
objectGuid: this.uuid,
|
||||
objectguid: this.uuid,
|
||||
uidNumber: uid_number,
|
||||
gidNumber: String(await this.get_uid_number()), // group_ids.map(x => String(x)),
|
||||
loginShell: shell,
|
||||
homeDirectory: `/home/${this.uid}@${domain}`
|
||||
}
|
||||
|
||||
if ( this.tagline ) ldap_data.extras_tagline = this.tagline
|
||||
@ -216,6 +330,10 @@ class User extends AuthUser {
|
||||
return LDAP.parseDN(`uid=${this.uid.toLowerCase()},${this.ldap_server.auth_dn().format(this.configs.get('ldap:server.format'))}`)
|
||||
}
|
||||
|
||||
get sudo_dn() {
|
||||
return LDAP.parseDN(`cn=sudo_${this.uid.toLowerCase()},${this.ldap_server.sudo_dn().format(this.configs.get('ldap:server.format'))}`)
|
||||
}
|
||||
|
||||
// The following are used by OpenID connect
|
||||
|
||||
async claims(use, scope) {
|
||||
|
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_id: String,
|
||||
access_type: String, // allow | deny
|
||||
target_type: { type: String, default: 'application' }, // application | api_scope
|
||||
target_type: { type: String, default: 'application' }, // application | api_scope | machine | machine_group
|
||||
target_id: String,
|
||||
active: { type: Boolean, default: true },
|
||||
for_permission: { type: Boolean, default: false },
|
||||
permission: String,
|
||||
}
|
||||
}
|
||||
|
||||
static async check_allow(entity_id, target_id) {
|
||||
static async check_allow(entity_id, target_id, permission = undefined) {
|
||||
const policies = await this.find({
|
||||
entity_id,
|
||||
target_id,
|
||||
access_type: 'allow',
|
||||
active: true,
|
||||
...(permission ? {
|
||||
for_permission: true,
|
||||
permission,
|
||||
} : {})
|
||||
})
|
||||
|
||||
return policies.length > 0
|
||||
}
|
||||
|
||||
static async check_deny(entity_id, target_id) {
|
||||
static async check_deny(entity_id, target_id, permission = undefined) {
|
||||
const policies = await this.find({
|
||||
entity_id,
|
||||
target_id,
|
||||
access_type: 'deny',
|
||||
active: true,
|
||||
...(permission ? {
|
||||
for_permission: true,
|
||||
permission,
|
||||
} : {})
|
||||
})
|
||||
|
||||
return policies.length === 0
|
||||
}
|
||||
|
||||
static async check_entity_access(entity_id, target_id) {
|
||||
return (await this.check_allow(entity_id, target_id)) && !(await this.check_deny(entity_id, target_id))
|
||||
static async check_entity_access(entity_id, target_id, permission = undefined) {
|
||||
return (await this.check_allow(entity_id, target_id, permission)) && !(await this.check_deny(entity_id, target_id, permission))
|
||||
}
|
||||
|
||||
static async check_user_denied(user, target_id) {
|
||||
static async check_user_denied(user, target_id, permission = undefined) {
|
||||
const groups = await user.groups()
|
||||
const group_ids = groups.map(x => x.id)
|
||||
|
||||
@ -53,6 +63,10 @@ class PolicyModel extends Model {
|
||||
target_id,
|
||||
access_type: 'deny',
|
||||
active: true,
|
||||
...(permission ? {
|
||||
for_permission: true,
|
||||
permission,
|
||||
} : {})
|
||||
})
|
||||
|
||||
const group_denials = await this.find({
|
||||
@ -60,41 +74,92 @@ class PolicyModel extends Model {
|
||||
target_id,
|
||||
access_type: 'deny',
|
||||
active: true,
|
||||
...(permission ? {
|
||||
for_permission: true,
|
||||
permission,
|
||||
} : {})
|
||||
})
|
||||
|
||||
return user_denials.length > 0 || group_denials.length > 0
|
||||
}
|
||||
|
||||
static async check_user_access(user, target_id) {
|
||||
static async get_all_related(target_id) {
|
||||
const all = [target_id]
|
||||
const Machine = this.prototype.models.get('ldap:Machine')
|
||||
const MachineGroup = this.prototype.models.get('ldap:MachineGroup')
|
||||
|
||||
const machine = await Machine.findById(target_id)
|
||||
if ( machine?.active ) {
|
||||
const groups = await MachineGroup.find({
|
||||
active: true,
|
||||
machine_ids: machine.id,
|
||||
})
|
||||
|
||||
groups.map(x => all.push(x.id))
|
||||
}
|
||||
|
||||
const group = await MachineGroup.findById(target_id)
|
||||
if ( group?.active ) {
|
||||
const machines = await Machine.find({
|
||||
active: true,
|
||||
_id: {
|
||||
$in: group.machine_ids.map(x => Machine.to_object_id(x)),
|
||||
}
|
||||
})
|
||||
|
||||
machines.map(x => all.push(x.id))
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
static async check_user_access(user, target_id, permission = undefined) {
|
||||
const groups = await user.groups()
|
||||
const group_ids = groups.map(x => x.id)
|
||||
const target_ids = await this.get_all_related(target_id)
|
||||
|
||||
const user_approvals = await this.find({
|
||||
entity_id: user.id,
|
||||
target_id,
|
||||
target_id: { $in: target_ids },
|
||||
access_type: 'allow',
|
||||
active: true,
|
||||
...(permission ? {
|
||||
for_permission: true,
|
||||
permission,
|
||||
} : {})
|
||||
})
|
||||
|
||||
const user_denials = await this.find({
|
||||
entity_id: user.id,
|
||||
target_id,
|
||||
target_id: { $in: target_ids },
|
||||
access_type: 'deny',
|
||||
active: true,
|
||||
...(permission ? {
|
||||
for_permission: true,
|
||||
permission,
|
||||
} : {})
|
||||
})
|
||||
|
||||
const group_approvals = await this.find({
|
||||
entity_id: { $in: group_ids },
|
||||
target_id,
|
||||
target_id: { $in: target_ids },
|
||||
access_type: 'allow',
|
||||
active: true,
|
||||
...(permission ? {
|
||||
for_permission: true,
|
||||
permission,
|
||||
} : {})
|
||||
})
|
||||
|
||||
const group_denials = await this.find({
|
||||
entity_id: { $in: group_ids },
|
||||
target_id,
|
||||
target_id: { $in: target_ids },
|
||||
access_type: 'deny',
|
||||
active: true,
|
||||
...(permission ? {
|
||||
for_permission: true,
|
||||
permission,
|
||||
} : {})
|
||||
})
|
||||
|
||||
// IF user has explicit denial, deny
|
||||
@ -132,6 +197,18 @@ class PolicyModel extends Model {
|
||||
target_display = `Application: ${app.name}`
|
||||
} else if ( this.target_type === 'api_scope' ) {
|
||||
target_display = `API Scope: ${this.target_id}`
|
||||
} else if ( this.target_type === 'machine' ) {
|
||||
const Machine = this.models.get('ldap:Machine')
|
||||
const machine = await Machine.findById(this.target_id)
|
||||
target_display = `Computer: ${machine.name}`
|
||||
|
||||
if ( machine.host_name ) {
|
||||
target_display += ` (${machine.host_name})`
|
||||
}
|
||||
} else if ( this.target_type === 'machine_group' ) {
|
||||
const MachineGroup = this.models.get('ldap:MachineGroup')
|
||||
const group = await MachineGroup.findById(this.target_id)
|
||||
target_display = `Computer Group: ${group.name} (${group.machine_ids.length} computers)`
|
||||
}
|
||||
|
||||
return {
|
||||
@ -143,6 +220,8 @@ class PolicyModel extends Model {
|
||||
target_display,
|
||||
target_type: this.target_type,
|
||||
target_id: this.target_id,
|
||||
for_permission: this.for_permission,
|
||||
permission: this.permission,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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
|
||||
const sp = await ServiceProvider.findOne({entity_id: data.issuer, active: true})
|
||||
if (!sp)
|
||||
return res.error(401, 'Unable to continue. The SAML issuer is unknown.')
|
||||
return res.error(401, {
|
||||
message: 'Unable to continue. The SAML issuer is unknown.'
|
||||
})
|
||||
|
||||
req.saml_request = {
|
||||
relay_state: req.query.RelayState || req.body.RelayState,
|
||||
|
@ -58,7 +58,29 @@ class TrapUtility {
|
||||
|
||||
allows(route) {
|
||||
const config = this.config()
|
||||
return route.startsWith('/assets') || config.allowed_routes.includes(route.toLowerCase().trim())
|
||||
const allowed = route.startsWith('/assets') || config.allowed_routes.includes(route.toLowerCase().trim())
|
||||
if ( allowed ) return true
|
||||
|
||||
for ( const allowed_route of config.allowed_routes ) {
|
||||
console.log('comparing', allowed_route, 'to', route)
|
||||
const allowed_parts = allowed_route.split('/')
|
||||
const parts = route.split('/')
|
||||
|
||||
let matches = true
|
||||
for ( let i = 0; i < allowed_parts.length; i += 1 ) {
|
||||
if ( allowed_parts[i] !== parts[i] && allowed_parts[i] !== '*' ) {
|
||||
matches = false
|
||||
}
|
||||
}
|
||||
|
||||
if ( matches ) {
|
||||
console.log('allows true')
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
console.log('allows false')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,8 +90,19 @@ class TrapsMiddleware extends Middleware {
|
||||
}
|
||||
|
||||
async test(req, res, next, args = {}) {
|
||||
const Setting = this.models.get('Setting')
|
||||
req.trap = new TrapUtility(req, res, this.configs.get('traps.types'))
|
||||
|
||||
if (
|
||||
!req.trap.has_trap()
|
||||
&& req.user
|
||||
&& !req.user.email_verified
|
||||
&& (await Setting.get('auth.require_email_verify'))
|
||||
) {
|
||||
req.session.email_verify_flow = req.originalUrl
|
||||
await req.trap.begin('verify_email', { session_only: false })
|
||||
}
|
||||
|
||||
if ( !req.trap.has_trap() ) return next()
|
||||
else if ( req.trap.allows(req.path) ) return next()
|
||||
else return req.trap.redirect()
|
||||
|
@ -36,6 +36,14 @@ const auth_routes = {
|
||||
['middleware::api:Permission', { check: 'v1:auth:users:get' }],
|
||||
'controller::api:v1:Auth.get_user',
|
||||
],
|
||||
'/users/:id/flat': [
|
||||
'middleware::auth:APIRoute',
|
||||
['middleware::api:Permission', { check: 'v1:auth:users:get' }],
|
||||
'controller::api:v1:Auth.get_user_flat',
|
||||
],
|
||||
'/users/:id/photo': [
|
||||
'controller::api:v1:Auth.get_user_photo',
|
||||
],
|
||||
'/groups/:id': [
|
||||
'middleware::auth:APIRoute',
|
||||
['middleware::api:Permission', { check: 'v1:auth:groups:get' }],
|
||||
|
@ -14,6 +14,14 @@ const iam_routes = {
|
||||
['middleware::api:Permission', { check: 'v1:iam:policy:get' }],
|
||||
'controller::api:v1:IAM.get_policy',
|
||||
],
|
||||
'/permission': [
|
||||
['middleware::api:Permission', { check: 'v1:iam:permission:list' }],
|
||||
'controller::api:v1:IAM.get_permissions',
|
||||
],
|
||||
'/permission/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:iam:permission:get' }],
|
||||
'controller::api:v1:IAM.get_permission',
|
||||
],
|
||||
},
|
||||
|
||||
post: {
|
||||
@ -21,6 +29,10 @@ const iam_routes = {
|
||||
['middleware::api:Permission', { check: 'v1:iam:policy:create' }],
|
||||
'controller::api:v1:IAM.create_policy',
|
||||
],
|
||||
'/permission': [
|
||||
['middleware::api:Permission', { check: 'v1:iam:permission:create' }],
|
||||
'controller::api:v1:IAM.create_permission',
|
||||
],
|
||||
'/check_entity_access': [
|
||||
['middleware::api:Permission', { check: 'v1:iam:check_entity_access' }],
|
||||
'controller::api:v1:IAM.check_entity_access',
|
||||
@ -36,6 +48,10 @@ const iam_routes = {
|
||||
['middleware::api:Permission', { check: 'v1:iam:policy:update' }],
|
||||
'controller::api:v1:IAM.update_policy',
|
||||
],
|
||||
'/permission/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:iam:permission:update' }],
|
||||
'controller::api:v1:IAM.update_permission',
|
||||
],
|
||||
},
|
||||
|
||||
delete: {
|
||||
@ -43,6 +59,10 @@ const iam_routes = {
|
||||
['middleware::api:Permission', { check: 'v1:iam:policy:delete' }],
|
||||
'controller::api:v1:IAM.delete_policy',
|
||||
],
|
||||
'/permission/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:iam:permission:delete' }],
|
||||
'controller::api:v1:IAM.delete_permission',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,22 @@ const ldap_routes = {
|
||||
['middleware::api:Permission', { check: 'v1:ldap:groups:get' }],
|
||||
'controller::api:v1:LDAP.get_group',
|
||||
],
|
||||
'/machines': [
|
||||
['middleware::api:Permission', { check: 'v1:ldap:machines:list' }],
|
||||
'controller::api:v1:LDAP.get_machines',
|
||||
],
|
||||
'/machines/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:ldap:machines:get' }],
|
||||
'controller::api:v1:LDAP.get_machine',
|
||||
],
|
||||
'/machine-groups': [
|
||||
['middleware::api:Permission', { check: 'v1:ldap:machine_groups:list' }],
|
||||
'controller::api:v1:LDAP.get_machine_groups',
|
||||
],
|
||||
'/machine-groups/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:ldap:machine_groups:get' }],
|
||||
'controller::api:v1:LDAP.get_machine_group',
|
||||
],
|
||||
'/config': [
|
||||
['middleware::api:Permission', { check: 'v1:ldap:config:get' }],
|
||||
'controller::api:v1:LDAP.get_config',
|
||||
@ -37,6 +53,14 @@ const ldap_routes = {
|
||||
['middleware::api:Permission', { check: 'v1:ldap:groups:create' }],
|
||||
'controller::api:v1:LDAP.create_group',
|
||||
],
|
||||
'/machines': [
|
||||
['middleware::api:Permission', { check: 'v1:ldap:machines:create' }],
|
||||
'controller::api:v1:LDAP.create_machine',
|
||||
],
|
||||
'/machine-groups': [
|
||||
['middleware::api:Permission', { check: 'v1:ldap:machine_groups:create' }],
|
||||
'controller::api:v1:LDAP.create_machine_group',
|
||||
],
|
||||
},
|
||||
|
||||
patch: {
|
||||
@ -48,6 +72,14 @@ const ldap_routes = {
|
||||
['middleware::api:Permission', { check: 'v1:ldap:groups:update' }],
|
||||
'controller::api:v1:LDAP.update_group',
|
||||
],
|
||||
'/machines/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:ldap:machines:update' }],
|
||||
'controller::api:v1:LDAP.update_machine',
|
||||
],
|
||||
'/machine-groups/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:ldap:machine_groups:update' }],
|
||||
'controller::api:v1:LDAP.update_machine_group',
|
||||
],
|
||||
},
|
||||
|
||||
delete: {
|
||||
@ -59,6 +91,14 @@ const ldap_routes = {
|
||||
['middleware::api:Permission', { check: 'v1:ldap:groups:delete' }],
|
||||
'controller::api:v1:LDAP.delete_group',
|
||||
],
|
||||
'/machines/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:ldap:machines:delete' }],
|
||||
'controller::api:v1:LDAP.delete_machine',
|
||||
],
|
||||
'/machine-groups/:id': [
|
||||
['middleware::api:Permission', { check: 'v1:ldap:machine_groups:delete' }],
|
||||
'controller::api:v1:LDAP.delete_machine_group',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
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',
|
||||
],
|
||||
|
||||
'/finish-registration': [
|
||||
'middleware::auth:UserOnly',
|
||||
'controller::auth:Forms.finish_registration',
|
||||
],
|
||||
|
||||
'/verify-email': [
|
||||
'middleware::auth:UserOnly',
|
||||
'controller::auth:Forms.show_verify_email',
|
||||
],
|
||||
|
||||
'/verify-email/sent': [
|
||||
'middleware::auth:UserOnly',
|
||||
'controller::auth:Forms.send_verify_email',
|
||||
],
|
||||
|
||||
'/login-message': [
|
||||
'middleware::auth:UserOnly',
|
||||
'controller::api:v1:System.show_login_message',
|
||||
|
@ -7,6 +7,9 @@ const openid = {
|
||||
],
|
||||
|
||||
get: {
|
||||
'/grant-and-save/:app_id/:uid': [
|
||||
'middleware::auth:UserOnly', 'controller::OpenID.grant_and_save',
|
||||
],
|
||||
'/interaction/:uid': [
|
||||
'controller::OpenID.handle_interaction',
|
||||
],
|
||||
|
@ -2,6 +2,7 @@ const Unit = require('libflitter/Unit')
|
||||
const LDAP = require('ldapjs')
|
||||
const Validator = require('email-validator')
|
||||
const net = require('net')
|
||||
const fs = require('fs')
|
||||
|
||||
// TODO support logging ALL ldap requests when in DEBUG, not just routed ones
|
||||
// TODO need to support LDAP server auto-discovery/detection features
|
||||
@ -36,6 +37,18 @@ class LDAPServerUnit extends Unit {
|
||||
return this.build_dn(this.config.schema.group_base)
|
||||
}
|
||||
|
||||
machine_dn() {
|
||||
return this.build_dn(this.config.schema.machine_base)
|
||||
}
|
||||
|
||||
machine_group_dn() {
|
||||
return this.build_dn(this.config.schema.machine_group_base)
|
||||
}
|
||||
|
||||
sudo_dn() {
|
||||
return this.build_dn(this.config.schema.sudo_base)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the anonymous DN.
|
||||
* @returns {ldap/DN}
|
||||
@ -77,7 +90,11 @@ class LDAPServerUnit extends Unit {
|
||||
|
||||
// If Flitter is configured to use an SSL certificate,
|
||||
// use it to enable LDAPS in the server.
|
||||
if ( this.express.use_ssl() ) {
|
||||
if ( this.config.ssl?.enable ) {
|
||||
this.output.info('Using configured SSL certificate. The LDAP server will require an ldaps:// connection.')
|
||||
server_config.certificate = fs.readFileSync(this.config.ssl.certificate)
|
||||
server_config.key = fs.readFileSync(this.config.ssl.key)
|
||||
} else if ( this.express.use_ssl() ) {
|
||||
this.output.info('Using configured SSL certificate. The LDAP server will require an ldaps:// connection.')
|
||||
server_config.certificate = await this.express.ssl_certificate()
|
||||
server_config.key = await this.express.ssl_key()
|
||||
|
@ -1,3 +1,4 @@
|
||||
const fs = require('fs')
|
||||
const Unit = require('libflitter/Unit')
|
||||
const { Provider, interactionPolicy: { Prompt, base: policy } } = require('oidc-provider')
|
||||
const uuid = require('uuid').v4
|
||||
@ -14,6 +15,15 @@ class OpenIDConnectUnit extends Unit {
|
||||
return [...super.services, 'output', 'configs', 'models']
|
||||
}
|
||||
|
||||
load_jwks(file) {
|
||||
if ( fs.existsSync(file) ) {
|
||||
const content = fs.readFileSync(file)
|
||||
try {
|
||||
return JSON.parse(content)
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
async go(app) {
|
||||
this.Vue = this.app.di().get('Vue')
|
||||
const issuer = this.configs.get('app.url')
|
||||
@ -23,9 +33,13 @@ class OpenIDConnectUnit extends Unit {
|
||||
|
||||
CoreIDAdapter.connect(app)
|
||||
|
||||
const jwks_file = this.configs.get('oidc.jwks_file')
|
||||
const jwks = this.load_jwks(jwks_file)
|
||||
|
||||
this.provider = new Provider(issuer, {
|
||||
adapter: CoreIDAdapter,
|
||||
clients: [],
|
||||
jwks,
|
||||
interactions: {
|
||||
interactions,
|
||||
url: (ctx, interaction) => `/openid/interaction/${ctx.oidc.uid.toLowerCase()}`,
|
||||
@ -58,6 +72,15 @@ class OpenIDConnectUnit extends Unit {
|
||||
...configuration,
|
||||
})
|
||||
|
||||
const reportError = ({ headers: { authorization }, oidc: { body, client } }, err) => {
|
||||
this.output.error('OpenIDConnect authorization error!')
|
||||
this.output.error(err)
|
||||
}
|
||||
|
||||
this.provider.on('grant.error', reportError)
|
||||
this.provider.on('introspection.error', reportError)
|
||||
this.provider.on('revocation.error', reportError)
|
||||
|
||||
if ( configuration.proxy ) this.provider.proxy = true
|
||||
app.express.use('/oidc', this.wrap(this.provider.callback))
|
||||
}
|
||||
@ -91,6 +114,11 @@ class OpenIDConnectUnit extends Unit {
|
||||
}
|
||||
}
|
||||
|
||||
// Stupid /jwks only listens on GET which is incompatible w/ some apps
|
||||
if ( req.url === '/jwks' ) {
|
||||
req.method = 'GET'
|
||||
}
|
||||
|
||||
return callback(req, res, next)
|
||||
}
|
||||
}
|
||||
|
63
app/unit/RadiusUnit.js
Normal file
63
app/unit/RadiusUnit.js
Normal file
@ -0,0 +1,63 @@
|
||||
const fs = require('fs/promises')
|
||||
const uuid = require('uuid')
|
||||
const { Unit } = require('libflitter')
|
||||
const CoreIDAuthentication = require('../classes/radius/CoreIDAuthentication')
|
||||
const net = require("net");
|
||||
|
||||
class RadiusUnit extends Unit {
|
||||
static get services() {
|
||||
return [...super.services, 'configs', 'output', 'models']
|
||||
}
|
||||
|
||||
async go(app) {
|
||||
if ( !this.configs.get('radius.enable') ) return;
|
||||
|
||||
const CoreIDRadiusServer = (await import('../classes/radius/CoreIDRadiusServer.mjs')).default
|
||||
|
||||
// Load the certificates
|
||||
const pubkey = await fs.readFile(this.configs.get('radius.cert_file.public'))
|
||||
const privkey = await fs.readFile(this.configs.get('radius.cert_file.private'))
|
||||
|
||||
this.radius = new CoreIDRadiusServer({
|
||||
// logger
|
||||
secret: this.configs.get('radius.secret', uuid.v4()),
|
||||
port: this.configs.get('radius.port', 1812),
|
||||
address: this.configs.get('radius.interface', '0.0.0.0'),
|
||||
tlsOptions: {
|
||||
cert: pubkey,
|
||||
key: privkey,
|
||||
},
|
||||
authentication: new CoreIDAuthentication(),
|
||||
})
|
||||
|
||||
if ( await this.port_free() ) {
|
||||
this.output.info('Starting RADIUS server...')
|
||||
await this.radius.start()
|
||||
} else {
|
||||
this.output.error('Will not start RADIUS server. Reason: configured port is already in use')
|
||||
delete this.radius
|
||||
}
|
||||
}
|
||||
|
||||
async cleanup(app) {
|
||||
if ( this.radius ) {
|
||||
await this.radius.server.close()
|
||||
}
|
||||
}
|
||||
|
||||
async port_free() {
|
||||
return new Promise((res, rej) => {
|
||||
const server = net.createServer()
|
||||
server.once('error', (e) => {
|
||||
res(false)
|
||||
})
|
||||
server.once('listening', () => {
|
||||
server.close()
|
||||
res(true)
|
||||
})
|
||||
server.listen(this.configs.get('radius.port', 1812))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = RadiusUnit
|
@ -13,6 +13,22 @@ class SettingsUnit extends Unit {
|
||||
Error.stackTraceLimit = 50
|
||||
app.express.set('trust proxy', true)
|
||||
|
||||
const User = this.models.get('auth:User')
|
||||
const user = await User.findOne({is_default_user_for_coreid: true})
|
||||
if ( !user ) {
|
||||
const user = new User({
|
||||
uid: '__coreid_default_user__',
|
||||
provider: 'flitter',
|
||||
block_login: true,
|
||||
first_name: 'Default_User',
|
||||
last_name: 'Default_User',
|
||||
ldap_visible: false,
|
||||
is_default_user_for_coreid: true,
|
||||
})
|
||||
|
||||
await user.save()
|
||||
}
|
||||
|
||||
const Setting = this.models.get('Setting')
|
||||
const default_settings = this.configs.get('setting.settings')
|
||||
for ( const key in default_settings ) {
|
||||
@ -21,6 +37,15 @@ class SettingsUnit extends Unit {
|
||||
this.output.debug(`Guarantee setting key "${key}" with default value "${default_value}".`)
|
||||
await Setting.guarantee(key, default_value)
|
||||
}
|
||||
|
||||
const Permission = this.models.get('iam:Permission')
|
||||
const default_permissions = this.configs.get('auth.iam.default_permissions')
|
||||
for ( const perm of default_permissions ) {
|
||||
const existing = await Permission.findOne(perm)
|
||||
if ( !existing ) {
|
||||
await (new Permission(perm)).save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,5 +4,4 @@ block content
|
||||
.cobalt-container
|
||||
.row.pad-top
|
||||
.col-12
|
||||
cobalt-form(v-if="form_id" :resource="resource" :form_id="form_id" :initial_mode="mode")
|
||||
cobalt-form(v-if="!form_id" :resource="resource" :initial_mode="mode")
|
||||
coreid-outlet(initial_page="cobalt.form" :initial_resource="resource" :initial_form_id="form_id" :initial_mode="mode")
|
||||
|
@ -4,4 +4,4 @@ block content
|
||||
.cobalt-container
|
||||
.row.pad-top
|
||||
.col-12
|
||||
cobalt-listing(:resource="resource")
|
||||
coreid-outlet(initial_page="cobalt.listing" :initial_resource="resource")
|
||||
|
@ -4,4 +4,4 @@ block content
|
||||
.cobalt-container
|
||||
.row.pad-top
|
||||
.col-12
|
||||
coreid-app-setup
|
||||
coreid-outlet(initial_page="app.setup")
|
||||
|
@ -3,5 +3,5 @@ extends ../../theme/dash/base
|
||||
block content
|
||||
.profile-container
|
||||
.row.pad-top
|
||||
.col-12.offset-0.col-md-8.offset-md-2.col-xl-6.offset-xl-3
|
||||
coreid-profile-edit
|
||||
.col-12
|
||||
coreid-outlet(initial_page="dash.profile")
|
||||
|
@ -18,4 +18,4 @@ block append content
|
||||
| people who self-host various applications. With its built-in OAuth2, LDAP, and SAML servers
|
||||
| and self-service password & admin panel, #{_app && _app.name || 'CoreID'} gives you the ability
|
||||
| to easily integrate a single-sign-on solution into your self-hosting infrastructure without
|
||||
| jumping through hoops to make it work. You can learn more <a href="https://wiki.garrettmills.dev/s/bsvhg1el9dtuppanj77g/starship-coreid">here.</a>
|
||||
| jumping through hoops to make it work. You can learn more <a href="https://garrettmills.dev/go/coreid">here.</a>
|
||||
|
@ -3,6 +3,19 @@ const auth_config = {
|
||||
default_provider: env('AUTH_DEFAULT_PROVIDER', 'flitter'),
|
||||
default_login_route: '/dash',
|
||||
|
||||
iam: {
|
||||
default_permissions: [
|
||||
{
|
||||
target_type: 'machine',
|
||||
permission: 'sudo',
|
||||
},
|
||||
{
|
||||
target_type: 'machine_group',
|
||||
permission: 'sudo',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
mfa: {
|
||||
secret_length: env('MFA_SECRET_LENGTH', 20)
|
||||
},
|
||||
@ -178,6 +191,7 @@ const auth_config = {
|
||||
ldap_client: ['ldap:bind', 'ldap:search'],
|
||||
coreid_base: ['my:profile'],
|
||||
saml_admin: ['v1:saml', 'saml'],
|
||||
radius_admin: ['v1:radius', 'radius'],
|
||||
|
||||
base_user: [
|
||||
// Message Service
|
||||
@ -197,7 +211,7 @@ const auth_config = {
|
||||
'ldap:search:users:me',
|
||||
],
|
||||
|
||||
root: ['v1', 'ldap', 'saml', 'profile', 'oauth', 'app', 'auth', 'iam'],
|
||||
root: ['v1', 'ldap', 'saml', 'profile', 'oauth', 'app', 'auth', 'iam', 'radius'],
|
||||
|
||||
},
|
||||
|
||||
|
@ -15,6 +15,7 @@ const jobs_config = {
|
||||
'mailer',
|
||||
'password_resets',
|
||||
'notifications',
|
||||
'verifications',
|
||||
],
|
||||
|
||||
// Mapping of worker name => worker config
|
||||
@ -23,7 +24,7 @@ const jobs_config = {
|
||||
// The name of the worker is "main"
|
||||
main: {
|
||||
// This worker will process these queues
|
||||
queues: ['mailer', 'password_resets', 'notifications'],
|
||||
queues: ['mailer', 'password_resets', 'notifications', 'verifications'],
|
||||
},
|
||||
|
||||
// You can have many workers, and multiple workers can
|
||||
@ -31,6 +32,13 @@ const jobs_config = {
|
||||
// worker processes of the same type.
|
||||
// (e.g. you can have two "main" workers)
|
||||
},
|
||||
|
||||
connector: {
|
||||
enabled: env('JOB_QUEUE_CONNECTOR', false),
|
||||
mount: env('JOB_QUEUE_CONNECTOR_MOUNT', '/job_queue_api'),
|
||||
secret: env('JOB_QUEUE_CONNECTOR_SECRET'),
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = jobs_config
|
||||
|
@ -5,13 +5,24 @@ const ldap_server = {
|
||||
max_connections: env('LDAP_MAX_CONNECTIONS'),
|
||||
interface: env('LDAP_LISTEN_INTERFACE', '0.0.0.0'),
|
||||
|
||||
ssl: {
|
||||
enable: env('LDAP_SSL_ENABLE', false),
|
||||
certificate: env('LDAP_CERT_PATH'),
|
||||
key: env('LDAP_CERT_KEY_PATH'),
|
||||
},
|
||||
|
||||
schema: {
|
||||
base_dc: env('LDAP_BASE_DC', 'dc=example,dc=com'),
|
||||
authentication_base: env('LDAP_AUTH_BASE', 'ou=people'),
|
||||
group_base: env('LDAP_GROUP_BASE', 'ou=groups'),
|
||||
machine_base: env('LDAP_MACHINE_BASE', 'ou=computers'),
|
||||
machine_group_base: env('LDAP_MACHINE_BASE', 'ou=computer groups'),
|
||||
sudo_base: env('LDAP_SUDO_BASE', 'ou=sudo'),
|
||||
auth: {
|
||||
user_id: 'uid',
|
||||
}
|
||||
},
|
||||
start_uid: env('LDAP_START_UID', 80000),
|
||||
default_shell: env('LDAP_DEFAULT_SHELL', '/bin/bash'),
|
||||
},
|
||||
|
||||
format: {
|
||||
|
@ -1,7 +1,8 @@
|
||||
const oidc_config = {
|
||||
provider: {
|
||||
proxy: env('OPENID_CONNECT_PROXY', false),
|
||||
}
|
||||
},
|
||||
jwks_file: env('OPENID_CONNECT_JWKS_FILE'),
|
||||
}
|
||||
|
||||
module.exports = exports = oidc_config
|
||||
|
15
config/radius.config.js
Normal file
15
config/radius.config.js
Normal file
@ -0,0 +1,15 @@
|
||||
const uuid = require('uuid')
|
||||
|
||||
const radius_config = {
|
||||
enable: env('RADIUS_ENABLE', false),
|
||||
port: env('RADIUS_PORT', 1812),
|
||||
interface: env('RADIUS_INTERFACE', '0.0.0.0'),
|
||||
secret: env('RADIUS_SECRET', uuid.v4()),
|
||||
|
||||
cert_file: {
|
||||
public: env('RADIUS_CERT_FILE'),
|
||||
private: env('RADIUS_KEY_FILE'),
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = radius_config
|
@ -10,6 +10,7 @@ const redis_config = {
|
||||
// https://github.com/luin/ioredis#connect-to-redis
|
||||
server: {
|
||||
host: env('REDIS_HOST', 'localhost'),
|
||||
password: env('REDIS_PASS'),
|
||||
port: env('REDIS_PORT', 6379),
|
||||
},
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
const setting_config = {
|
||||
settings: {
|
||||
'auth.allow_registration': true,
|
||||
'auth.require_email_verify': false,
|
||||
'auth.default_roles': [ 'base_user' ],
|
||||
'home.allow_landing': true,
|
||||
'home.redirect_authenticated': true,
|
||||
'ldap.last_alloc_uid': -1,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,32 @@ const traps_config = {
|
||||
'/auth/login-message/dismiss',
|
||||
],
|
||||
},
|
||||
registrant_flow: {
|
||||
redirect_to: '/auth/finish-registration',
|
||||
allowed_routes: [
|
||||
'/auth/finish-registration',
|
||||
'/auth/logout',
|
||||
'/auth/login',
|
||||
'/api/v1/locale/batch',
|
||||
'/api/v1/auth/validate/username',
|
||||
'/api/v1/auth/attempt',
|
||||
],
|
||||
},
|
||||
verify_email: {
|
||||
redirect_to: '/auth/verify-email',
|
||||
allowed_routes: [
|
||||
'/auth/verify-email',
|
||||
'/auth/verify-email/sent',
|
||||
'/auth/logout',
|
||||
'/auth/login',
|
||||
'/api/v1/locale/batch',
|
||||
'/api/v1/auth/validate/username',
|
||||
'/api/v1/auth/attempt',
|
||||
'/auth/action/*',
|
||||
'/api/v1/message/banners',
|
||||
'/api/v1/message/banners/read/*',
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
4
deploy/0-namespace.yaml
Normal file
4
deploy/0-namespace.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: starship
|
217
deploy/1-deployment.yaml
Normal file
217
deploy/1-deployment.yaml
Normal file
@ -0,0 +1,217 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: coreid-www
|
||||
namespace: starship
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: coreid
|
||||
template:
|
||||
metadata:
|
||||
name: coreid
|
||||
labels:
|
||||
app: coreid
|
||||
spec:
|
||||
volumes:
|
||||
- name: coreid-secrets-vol
|
||||
secret:
|
||||
secretName: coreid-secrets
|
||||
optional: false
|
||||
containers:
|
||||
- name: coreid-web
|
||||
image: ${DOCKER_REGISTRY}/starship/coreid
|
||||
imagePullPolicy: Always
|
||||
volumeMounts:
|
||||
- mountPath: /secrets
|
||||
readOnly: true
|
||||
name: coreid-secrets-vol
|
||||
env:
|
||||
- name: APP_URL
|
||||
value: "https://${COREID_DOMAIN}/"
|
||||
- name: DATABASE_HOST
|
||||
value: '${COREID_DATABASE_HOST}'
|
||||
- name: DATABASE_NAME
|
||||
value: '${COREID_DATABASE_NAME}'
|
||||
- name: LDAP_BASE_DC
|
||||
value: '${COREID_LDAP_BASE_DC}'
|
||||
- name: REDIS_HOST
|
||||
value: '${COREID_REDIS_HOST}'
|
||||
- name: SMTP_HOST
|
||||
value: '${COREID_SMTP_HOST}'
|
||||
- name: SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: SECRET
|
||||
name: coreid-secrets
|
||||
optional: false
|
||||
- name: SMTP_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: SMTP_USER
|
||||
name: coreid-secrets
|
||||
optional: false
|
||||
- name: SMTP_DEFAULT_SENDER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: SMTP_DEFAULT_SENDER
|
||||
name: coreid-secrets
|
||||
optional: false
|
||||
- name: SMTP_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: SMTP_PASS
|
||||
name: coreid-secrets
|
||||
optional: false
|
||||
- name: REDIS_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: REDIS_PASS
|
||||
name: coreid-secrets
|
||||
optional: false
|
||||
- name: APP_NAME
|
||||
value: "Starship CoreID"
|
||||
- name: SERVER_PORT
|
||||
value: '8000'
|
||||
- name: DATABASE_PORT
|
||||
value: '27017'
|
||||
- name: DATABASE_AUTH
|
||||
value: 'false'
|
||||
- name: ENVIRONMENT
|
||||
value: production
|
||||
- name: SSL_ENABLE
|
||||
value: 'false'
|
||||
- name: LDAP_SERVER_PORT
|
||||
value: '636'
|
||||
- name: LDAP_SSL_ENABLE
|
||||
value: 'true'
|
||||
- name: LDAP_CERT_PATH
|
||||
value: '/secrets/X509_CERT'
|
||||
- name: LDAP_CERT_KEY_PATH
|
||||
value: '/secrets/X509_KEY'
|
||||
- name: SAML_CERT_FILE
|
||||
value: '/secrets/X509_CERT'
|
||||
- name: SAML_KEY_FILE
|
||||
value: '/secrets/X509_KEY'
|
||||
- name: RADIUS_CERT_FILE
|
||||
value: '/secrets/X509_CERT'
|
||||
- name: RADIUS_KEY_FILE
|
||||
value: '/secrets/X509_KEY'
|
||||
- name: REDIS_PORT
|
||||
value: '6379'
|
||||
- name: SMTP_PORT
|
||||
value: '587'
|
||||
- name: OPENID_CONNECT_PROXY
|
||||
value: 'true'
|
||||
- name: SESSION_MAX_AGE
|
||||
value: '1209600000'
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: coreid-jobs
|
||||
namespace: starship
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: coreid-jobs
|
||||
template:
|
||||
metadata:
|
||||
name: coreid
|
||||
labels:
|
||||
app: coreid-jobs
|
||||
spec:
|
||||
volumes:
|
||||
- name: coreid-secrets-vol
|
||||
secret:
|
||||
secretName: coreid-secrets
|
||||
optional: false
|
||||
containers:
|
||||
- name: coreid-job-worker
|
||||
image: ${DOCKER_REGISTRY}/starship/coreid
|
||||
imagePullPolicy: Always
|
||||
command: ["node", "/app/flitter", "worker", "main"]
|
||||
volumeMounts:
|
||||
- mountPath: /secrets
|
||||
readOnly: true
|
||||
name: coreid-secrets-vol
|
||||
env:
|
||||
- name: APP_URL
|
||||
value: "https://${COREID_DOMAIN}/"
|
||||
- name: DATABASE_HOST
|
||||
value: '${COREID_DATABASE_HOST}'
|
||||
- name: DATABASE_NAME
|
||||
value: '${COREID_DATABASE_NAME}'
|
||||
- name: LDAP_BASE_DC
|
||||
value: '${COREID_LDAP_BASE_DC}'
|
||||
- name: REDIS_HOST
|
||||
value: '${COREID_REDIS_HOST}'
|
||||
- name: SMTP_HOST
|
||||
value: '${COREID_SMTP_HOST}'
|
||||
- name: SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: SECRET
|
||||
name: coreid-secrets
|
||||
optional: false
|
||||
- name: SMTP_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: SMTP_USER
|
||||
name: coreid-secrets
|
||||
optional: false
|
||||
- name: SMTP_DEFAULT_SENDER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: SMTP_DEFAULT_SENDER
|
||||
name: coreid-secrets
|
||||
optional: false
|
||||
- name: SMTP_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: SMTP_PASS
|
||||
name: coreid-secrets
|
||||
optional: false
|
||||
- name: REDIS_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: REDIS_PASS
|
||||
name: coreid-secrets
|
||||
optional: false
|
||||
- name: APP_NAME
|
||||
value: "Starship CoreID"
|
||||
- name: SERVER_PORT
|
||||
value: '8000'
|
||||
- name: DATABASE_PORT
|
||||
value: '27017'
|
||||
- name: DATABASE_AUTH
|
||||
value: 'false'
|
||||
- name: ENVIRONMENT
|
||||
value: production
|
||||
- name: SSL_ENABLE
|
||||
value: 'false'
|
||||
- name: LDAP_SERVER_PORT
|
||||
value: '636'
|
||||
- name: LDAP_SSL_ENABLE
|
||||
value: 'true'
|
||||
- name: LDAP_CERT_PATH
|
||||
value: '/secrets/X509_CERT'
|
||||
- name: LDAP_CERT_KEY_PATH
|
||||
value: '/secrets/X509_KEY'
|
||||
- name: SAML_CERT_FILE
|
||||
value: '/secrets/X509_CERT'
|
||||
- name: SAML_KEY_FILE
|
||||
value: '/secrets/X509_KEY'
|
||||
- name: RADIUS_CERT_FILE
|
||||
value: '/secrets/X509_CERT'
|
||||
- name: RADIUS_KEY_FILE
|
||||
value: '/secrets/X509_KEY'
|
||||
- name: REDIS_PORT
|
||||
value: '6379'
|
||||
- name: SMTP_PORT
|
||||
value: '587'
|
||||
- name: OPENID_CONNECT_PROXY
|
||||
value: 'true'
|
||||
- name: SESSION_MAX_AGE
|
||||
value: '1209600000'
|
24
deploy/2-service.yaml
Normal file
24
deploy/2-service.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: coreid-web
|
||||
namespace: starship
|
||||
spec:
|
||||
selector:
|
||||
app: coreid
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8000
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: coreid-ldaps
|
||||
namespace: starship
|
||||
spec:
|
||||
selector:
|
||||
app: coreid
|
||||
ports:
|
||||
- port: 636
|
||||
targetPort: 636
|
13
deploy/3-certificate.yaml
Normal file
13
deploy/3-certificate.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: coreid-tls
|
||||
namespace: starship
|
||||
spec:
|
||||
secretName: coreid-tls-secret
|
||||
dnsNames:
|
||||
- ${COREID_DOMAIN}
|
||||
issuerRef:
|
||||
name: letsencrypt-ca
|
||||
kind: ClusterIssuer
|
25
deploy/4-ingress.yaml
Normal file
25
deploy/4-ingress.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: coreid-ingress
|
||||
namespace: starship
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: 'false'
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- ${COREID_DOMAIN}
|
||||
secretName: coreid-tls-secret
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: ${COREID_DOMAIN}
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: '/'
|
||||
backend:
|
||||
service:
|
||||
name: coreid-web
|
||||
port:
|
||||
number: 80
|
32
deploy/README.md
Normal file
32
deploy/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
This deployment is parameterized for use with `envsubst(1)`.
|
||||
|
||||
You will need to set up the secret values and environment variables (see below).
|
||||
|
||||
```shell
|
||||
bash -c 'for f in *.yaml; do envsubst < $f | kubectl apply -f -; done'
|
||||
```
|
||||
|
||||
## Supported environment variables
|
||||
|
||||
Set these environment variables in your shell before running the above command to apply the Kubernetes spec.
|
||||
|
||||
- `COREID_DOMAIN` - domain name where CoreID is accessed (e.g. `coreid.mydomain.com`)
|
||||
- `DOCKER_REGISTRY` - host of the docker registry to pull the image from (e.g. `registry.mydomain.com`)
|
||||
- this is the same registry that is used by `yarn docker:build` and `yarn docker:push`
|
||||
- `COREID_DATABASE_HOST` - MongoDB host (e.g. `mongo.mylan.net`)
|
||||
- `COREID_DATABASE_NAME` - MongoDB database name to use (e.g. `coreid_p1`)
|
||||
- `COREID_LDAP_BASE_DC` - base DC to use for LDAP tree (e.g. `dc=platform,dc=local`)
|
||||
- `COREID_REDIS_HOST` - Redis host (e.g. `redis.mylan.net`)
|
||||
- `COREID_SMTP_HOST` - SMTP server host (e.g. `smtp.mymail.com`)
|
||||
|
||||
## Secret values
|
||||
|
||||
The spec expects there to be a `coreid-secrets` secret in the `starship` namespace with the following values:
|
||||
|
||||
- `SECRET` - hash seed used by CoreID (e.g. `df8db5a2-429b-4597-a013-18efee2465e0`)
|
||||
- `SMTP_USER` - username used to log-into SMTP server (e.g. `user@mymail.com`)
|
||||
- `SMTP_DEFAULT_SENDER` - email to use as FROM address. Usually same as `SMTP_USER` (e.g. `user@mymail.com`)
|
||||
- `SMTP_PASS` - password for `SMTP_USER`
|
||||
- `REDIS_PASS` - password for the Redis service
|
||||
- `X509_CERT` - contents of the x509 certificate to be used for SAML/LDAP/RADIUS
|
||||
- `X509_KEY` - contents of the x509 certificate key to be used for SAML/LDAP/RADIUS
|
@ -33,3 +33,6 @@ SMTP_PORT="587"
|
||||
SMTP_USER="coreid@localhost.localdomain"
|
||||
SMTP_DEFAULT_SENDER="coreid@localhost.localdomain"
|
||||
SMTP_PASS="something super secure"
|
||||
|
||||
JOB_QUEUE_CONNECTOR=true
|
||||
JOB_QUEUE_CONNECTOR_SECRET=
|
||||
|
2
index.js
2
index.js
@ -1,3 +1,5 @@
|
||||
Error.stackTraceLimit = 200
|
||||
|
||||
/*
|
||||
* Load the units file.
|
||||
* -------------------------------------------------------------
|
||||
|
@ -3,9 +3,12 @@ module.exports = exports = {
|
||||
application_already_exists: 'An Application with that identifier already exists.',
|
||||
|
||||
group_not_found: 'Group not found with that ID.',
|
||||
machine_not_found: 'Machine not found with that ID.',
|
||||
group_already_exists: 'A group with that name already exists.',
|
||||
machine_already_exists: 'A machine with that name already exists.',
|
||||
|
||||
user_not_found: 'User not found with that ID.',
|
||||
photo_not_found: 'This user has no photo.',
|
||||
user_already_exists: 'A user with that identifier already exists.',
|
||||
|
||||
client_not_found: 'Client not found with that ID.',
|
||||
@ -14,6 +17,8 @@ module.exports = exports = {
|
||||
token_not_found: 'Token not found with that ID, or the token has expired.',
|
||||
|
||||
provider_already_exists: 'A service provider with that entity_id already exists.',
|
||||
permission_already_exists: 'A permission for that target_type already exists.',
|
||||
permission_not_found: 'Permission not found with that ID.',
|
||||
|
||||
setting_not_found: 'No such setting exists with that key.',
|
||||
|
||||
@ -25,7 +30,9 @@ module.exports = exports = {
|
||||
|
||||
invalid_ldap_client_id: 'Invalid ldap_client_id:',
|
||||
invalid_oauth_client_id: 'Invalid oauth_client_id:',
|
||||
invalid_radius_client_id: 'Invalid radius_client_id:',
|
||||
invalid_saml_service_provider_id: 'Invalid saml_service_provider_id:',
|
||||
invalid_target_type: 'Invalid target_type.',
|
||||
|
||||
insufficient_permissions: 'Insufficient permissions.',
|
||||
missing_field: {
|
||||
|
@ -26,4 +26,6 @@ module.exports = exports = {
|
||||
oauth_prompt: 'CLIENT_NAME is requesting access to your APP_NAME account. Once you grant it, you may not be prompted for permission again.',
|
||||
will_redirect: 'You will be redirected to:',
|
||||
reauth_to_continue: 'Please re-authenticate to continue.',
|
||||
must_verify_email: 'You must verify your e-mail address to continue. Click below to send the verification e-mail.',
|
||||
verify_email_sent: 'Check your e-mail for the link to verify your account.',
|
||||
}
|
||||
|
5
locale/en_US/authn.locale.js
Normal file
5
locale/en_US/authn.locale.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = exports = {
|
||||
authn: 'Biometric/Hardware Authentication',
|
||||
enable: 'Setup Hardware Login',
|
||||
desc: 'On supported devices, you can log into your APP_NAME account using a hardware token or biometric sensor such as your phone\'s fingerprint reader.',
|
||||
}
|
@ -7,11 +7,13 @@ module.exports = exports = {
|
||||
|
||||
invalid: 'Invalid',
|
||||
unnamed: '(unnamed)',
|
||||
never: 'Never',
|
||||
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
deny: 'Deny',
|
||||
grant: 'Grant Access',
|
||||
grant: 'Allow access',
|
||||
grant_once: 'Allow access once',
|
||||
back: 'Back',
|
||||
next: 'Next',
|
||||
cancel: 'Cancel',
|
||||
|
@ -1,4 +1,5 @@
|
||||
module.exports = exports = {
|
||||
policy_not_found: 'Policy not found with that ID.',
|
||||
permission_not_found: 'Permission not found with that ID.',
|
||||
invalid_entity: 'Invalid entity_type. Must be one of:'
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ module.exports = exports = {
|
||||
test_notification: 'This is a test notification! If you see this, it means you have properly configured your notification settings.',
|
||||
|
||||
issued: 'Issued:',
|
||||
accessed: 'Last Used:',
|
||||
gen_new: 'Generate New',
|
||||
recovery_codes: 'Recovery Codes',
|
||||
recovery_1: 'Recovery codes can be used to regain access to your account in the event that you lose access to the device that generates your MFA codes.',
|
||||
@ -51,4 +52,7 @@ module.exports = exports = {
|
||||
|
||||
mfa_1: 'MFA is a good-practice security measure that requires you to provide a second factor of identification when you sign in from a service or device that makes use of APP_NAME.',
|
||||
mfa_2: 'Once enabled, APP_NAME will prompt you to enter a code when you sign-in with the APP_NAME web interface from a new device. It will also require you to append the code to your password when signing in to a service that uses APP_NAME as a backend.',
|
||||
|
||||
advanced_header: 'Advanced',
|
||||
advanced_shell: 'Default Login Shell',
|
||||
}
|
||||
|
13
package.json
13
package.json
@ -13,19 +13,25 @@
|
||||
"framework",
|
||||
"express"
|
||||
],
|
||||
"scripts": {
|
||||
"docker:build": "docker build -t ${DOCKER_REGISTRY}/starship/coreid .",
|
||||
"docker:push": "docker push ${DOCKER_REGISTRY}/starship/coreid"
|
||||
},
|
||||
"author": "Garrett Mills <garrett@glmdev.tech> (https://garrettmills.dev/)",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@coreid/radius-server": "^2.2.2",
|
||||
"@passwordless-id/webauthn": "^1.2.0",
|
||||
"bullmq": "^1.8.8",
|
||||
"email-validator": "^2.0.4",
|
||||
"flitter-auth": "^0.19.1",
|
||||
"flitter-auth": "^0.19.6",
|
||||
"flitter-cli": "^0.16.0",
|
||||
"flitter-di": "^0.5.0",
|
||||
"flitter-flap": "^0.5.2",
|
||||
"flitter-forms": "^0.8.1",
|
||||
"flitter-gotify": "^0.1.0",
|
||||
"flitter-i18n": "^0.1.1",
|
||||
"flitter-jobs": "^0.1.2",
|
||||
"flitter-jobs": "^0.4.0",
|
||||
"flitter-less": "^0.5.3",
|
||||
"flitter-orm": "^0.4.0",
|
||||
"flitter-redis": "^0.1.1",
|
||||
@ -33,12 +39,13 @@
|
||||
"ioredis": "^4.17.1",
|
||||
"is-absolute-url": "^3.0.3",
|
||||
"ldapjs": "^1.0.2",
|
||||
"libflitter": "^0.57.0",
|
||||
"libflitter": "^0.59.0",
|
||||
"moment": "^2.24.0",
|
||||
"mongodb": "^3.5.9",
|
||||
"nodemailer": "^6.4.6",
|
||||
"oidc-provider": "^6.29.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"radius": "^1.1.4",
|
||||
"samlp": "^3.4.1",
|
||||
"speakeasy": "^2.0.0",
|
||||
"uuid": "^8.3.0",
|
||||
|
Loading…
Reference in New Issue
Block a user