17 radius (#123)

Majority of radius integration and tooling complete, including docker files.
This commit is contained in:
Firstyear 2019-10-31 10:48:15 +10:00 committed by GitHub
parent 86938a7521
commit c006341884
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 3467 additions and 274 deletions

View file

@ -2,4 +2,5 @@ target
.git
.gitignore
test.db
vendor

1
.gitignore vendored
View file

@ -6,3 +6,4 @@
**/*.rs.bk
test.db
/vendor
kanidm_rlm_python/test_data/certs/

View file

@ -62,12 +62,12 @@ In a new terminal, you can now build and run the client tools with:
cd kanidm_tools
cargo run -- --help
cargo run -- whoami -H https://localhost:8080 -D anonymous -C ../insecure/ca.pem
cargo run -- whoami -H https://localhost:8080 -D admin -C ../insecure/ca.pem
cargo run -- self whoami -H https://localhost:8080 -D anonymous -C ../insecure/ca.pem
cargo run -- self whoami -H https://localhost:8080 -D admin -C ../insecure/ca.pem
For more see [getting started]
[getting started]: https://github.com/Firstyear/kanidm/blob/master/GETTING_STARTED.html
[getting started]: https://github.com/Firstyear/kanidm/blob/master/GETTING_STARTED.md
## Development and Testing

View file

@ -12,8 +12,9 @@ use std::io::Read;
use kanidm_proto::v1::{
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest,
Entry, Filter, ModifyList, ModifyRequest, OperationError, OperationResponse, SearchRequest,
SearchResponse, SetAuthCredential, SingleStringRequest, UserAuthToken, WhoamiResponse,
Entry, Filter, ModifyList, ModifyRequest, OperationError, OperationResponse, RadiusAuthToken,
SearchRequest, SearchResponse, SetAuthCredential, SingleStringRequest, UserAuthToken,
WhoamiResponse,
};
use serde_json;
@ -155,6 +156,22 @@ impl KanidmClient {
Ok(r)
}
fn perform_delete_request(&self, dest: &str) -> Result<(), ClientError> {
let dest = format!("{}{}", self.addr, dest);
let mut response = self
.client
.delete(dest.as_str())
.send()
.map_err(|e| ClientError::Transport(e))?;
match response.status() {
reqwest::StatusCode::OK => {}
unexpect => return Err(ClientError::Http(unexpect, response.json().ok())),
}
Ok(())
}
// whoami
// Can't use generic get due to possible un-auth case.
pub fn whoami(&self) -> Result<Option<(Entry, UserAuthToken)>, ClientError> {
@ -324,6 +341,28 @@ impl KanidmClient {
})
}
pub fn idm_account_radius_credential_get(
&self,
id: &str,
) -> Result<Option<String>, ClientError> {
self.perform_get_request(format!("/v1/account/{}/_radius", id).as_str())
}
pub fn idm_account_radius_credential_regenerate(
&self,
id: &str,
) -> Result<String, ClientError> {
self.perform_post_request(format!("/v1/account/{}/_radius", id).as_str(), ())
}
pub fn idm_account_radius_credential_delete(&self, id: &str) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/account/{}/_radius", id).as_str())
}
pub fn idm_account_radius_token_get(&self, id: &str) -> Result<RadiusAuthToken, ClientError> {
self.perform_get_request(format!("/v1/account/{}/_radius/_token", id).as_str())
}
// ==== schema
pub fn idm_schema_list(&self) -> Result<Vec<Entry>, ClientError> {
self.perform_get_request("/v1/schema")

View file

@ -335,4 +335,50 @@ fn test_server_rest_schema_read() {
});
}
// Test resetting a radius cred, and then checking/viewing it.
#[test]
fn test_server_radius_credential_lifecycle() {
run_test(|rsclient: KanidmClient| {
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
assert!(res.is_ok());
// Should have no radius secret
let n_sec = rsclient.idm_account_radius_credential_get("admin").unwrap();
assert!(n_sec.is_none());
// Set one
let sec1 = rsclient
.idm_account_radius_credential_regenerate("admin")
.unwrap();
// Should be able to get it.
let r_sec = rsclient.idm_account_radius_credential_get("admin").unwrap();
assert!(sec1 == r_sec.unwrap());
// test getting the token - we can do this as self or the radius server
let r_tok = rsclient.idm_account_radius_token_get("admin").unwrap();
assert!(sec1 == r_tok.secret);
assert!(r_tok.name == "admin".to_string());
// Reset it
let sec2 = rsclient
.idm_account_radius_credential_regenerate("admin")
.unwrap();
// Should be different
println!("s1 {} != s2 {}", sec1, sec2);
assert!(sec1 != sec2);
// Delete it
let res = rsclient.idm_account_radius_credential_delete("admin");
assert!(res.is_ok());
// No secret
let n_sec = rsclient.idm_account_radius_credential_get("admin").unwrap();
assert!(n_sec.is_none());
});
}
// Test the self version of the radius path.
// Test hitting all auth-required endpoints and assert they give unauthorized.

View file

@ -139,6 +139,27 @@ impl fmt::Display for UserAuthToken {
// UAT will need a downcast to Entry, which adds in the claims to the entry
// for the purpose of filtering.
// This is similar to uat, but omits claims (they have no role in radius), and adds
// the radius secret field.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RadiusAuthToken {
pub name: String,
pub displayname: String,
pub uuid: String,
pub secret: String,
pub groups: Vec<Group>,
}
impl fmt::Display for RadiusAuthToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "name: {}", self.name)?;
writeln!(f, "display: {}", self.displayname)?;
writeln!(f, "uuid: {}", self.uuid)?;
writeln!(f, "secret: {}", self.secret)?;
writeln!(f, "groups: {:?}", self.groups)
}
}
/* ===== low level proto types ===== */
// ProtoEntry vs Entry

View file

@ -0,0 +1,36 @@
FROM opensuse/leap:latest
MAINTAINER william@blackhats.net.au
EXPOSE 1812 1813
RUN zypper install -y timezone freeradius-client freeradius-server freeradius-server-ldap \
freeradius-server-python openldap2-client freeradius-server-utils hostname \
python2 python2-requests && \
zypper clean
# Copy the python module to /etc/raddb
COPY kanidmradius.py /etc/raddb/
COPY entrypoint.py /entrypoint.py
# Copy in the python changes, as well as the default/inner-tunnel changes
COPY mod-python /etc/raddb/mods-available/python
COPY default /etc/raddb/sites-available/default
COPY inner-tunnel /etc/raddb/sites-available/inner-tunnel
# Enable the python module.
RUN ln -s ../mods-available/python /etc/raddb/mods-enabled/python
# Allows radiusd (?) to write to the directory
RUN chown -R radiusd: /etc/raddb && \
chmod 775 /etc/raddb/certs && \
chmod 640 /etc/raddb/clients.conf
# Set a working directory of /etc/raddb
WORKDIR /etc/raddb
# /data volume
VOLUME /data
USER radiusd
CMD [ "/usr/bin/python2", "/entrypoint.py" ]

View file

@ -0,0 +1,25 @@
[kanidm_client]
url =
strict = false
ca = /data/ca.crt
user =
secret =
required_group =
; default vlans for groups that don't specify one.
[DEFAULT]
vlan = 1
; [group.test]
; vlan =
[radiusd]
ca =
key =
cert =
dh =
; [client.localhost]
; ipaddr =
; secret =

964
kanidm_rlm_python/default Normal file
View file

@ -0,0 +1,964 @@
######################################################################
#
# As of 2.0.0, FreeRADIUS supports virtual hosts using the
# "server" section, and configuration directives.
#
# Virtual hosts should be put into the "sites-available"
# directory. Soft links should be created in the "sites-enabled"
# directory to these files. This is done in a normal installation.
#
# If you are using 802.1X (EAP) authentication, please see also
# the "inner-tunnel" virtual server. You will likely have to edit
# that, too, for authentication to work.
#
# $Id: cfb973a9a8fd3d83e8e30c0599ddb911a3bdde9b $
#
######################################################################
#
# Read "man radiusd" before editing this file. See the section
# titled DEBUGGING. It outlines a method where you can quickly
# obtain the configuration you want, without running into
# trouble. See also "man unlang", which documents the format
# of this file.
#
# This configuration is designed to work in the widest possible
# set of circumstances, with the widest possible number of
# authentication methods. This means that in general, you should
# need to make very few changes to this file.
#
# The best way to configure the server for your local system
# is to CAREFULLY edit this file. Most attempts to make large
# edits to this file will BREAK THE SERVER. Any edits should
# be small, and tested by running the server with "radiusd -X".
# Once the edits have been verified to work, save a copy of these
# configuration files somewhere. (e.g. as a "tar" file). Then,
# make more edits, and test, as above.
#
# There are many "commented out" references to modules such
# as ldap, sql, etc. These references serve as place-holders.
# If you need the functionality of that module, then configure
# it in radiusd.conf, and un-comment the references to it in
# this file. In most cases, those small changes will result
# in the server being able to connect to the DB, and to
# authenticate users.
#
######################################################################
server default {
#
# If you want the server to listen on additional addresses, or on
# additional ports, you can use multiple "listen" sections.
#
# Each section make the server listen for only one type of packet,
# therefore authentication and accounting have to be configured in
# different sections.
#
# The server ignore all "listen" section if you are using '-i' and '-p'
# on the command line.
#
listen {
# Type of packets to listen for.
# Allowed values are:
# auth listen for authentication packets
# acct listen for accounting packets
# proxy IP to use for sending proxied packets
# detail Read from the detail file. For examples, see
# raddb/sites-available/copy-acct-to-home-server
# status listen for Status-Server packets. For examples,
# see raddb/sites-available/status
# coa listen for CoA-Request and Disconnect-Request
# packets. For examples, see the file
# raddb/sites-available/coa
#
type = auth
# Note: "type = proxy" lets you control the source IP used for
# proxying packets, with some limitations:
#
# * A proxy listener CANNOT be used in a virtual server section.
# * You should probably set "port = 0".
# * Any "clients" configuration will be ignored.
#
# See also proxy.conf, and the "src_ipaddr" configuration entry
# in the sample "home_server" section. When you specify the
# source IP address for packets sent to a home server, the
# proxy listeners are automatically created.
# ipaddr/ipv4addr/ipv6addr - IP address on which to listen.
# If multiple ones are listed, only the first one will
# be used, and the others will be ignored.
#
# The configuration options accept the following syntax:
#
# ipv4addr - IPv4 address (e.g.192.0.2.3)
# - wildcard (i.e. *)
# - hostname (radius.example.com)
# Only the A record for the host name is used.
# If there is no A record, an error is returned,
# and the server fails to start.
#
# ipv6addr - IPv6 address (e.g. 2001:db8::1)
# - wildcard (i.e. *)
# - hostname (radius.example.com)
# Only the AAAA record for the host name is used.
# If there is no AAAA record, an error is returned,
# and the server fails to start.
#
# ipaddr - IPv4 address as above
# - IPv6 address as above
# - wildcard (i.e. *), which means IPv4 wildcard.
# - hostname
# If there is only one A or AAAA record returned
# for the host name, it is used.
# If multiple A or AAAA records are returned
# for the host name, only the first one is used.
# If both A and AAAA records are returned
# for the host name, only the A record is used.
#
# ipv4addr = *
# ipv6addr = *
ipaddr = *
# Port on which to listen.
# Allowed values are:
# integer port number (1812)
# 0 means "use /etc/services for the proper port"
port = 0
# Some systems support binding to an interface, in addition
# to the IP address. This feature isn't strictly necessary,
# but for sites with many IP addresses on one interface,
# it's useful to say "listen on all addresses for eth0".
#
# If your system does not support this feature, you will
# get an error if you try to use it.
#
# interface = eth0
# Per-socket lists of clients. This is a very useful feature.
#
# The name here is a reference to a section elsewhere in
# radiusd.conf, or clients.conf. Having the name as
# a reference allows multiple sockets to use the same
# set of clients.
#
# If this configuration is used, then the global list of clients
# is IGNORED for this "listen" section. Take care configuring
# this feature, to ensure you don't accidentally disable a
# client you need.
#
# See clients.conf for the configuration of "per_socket_clients".
#
# clients = per_socket_clients
#
# Set the default UDP receive buffer size. In most cases,
# the default values set by the kernel are fine. However, in
# some cases the NASes will send large packets, and many of
# them at a time. It is then possible to overflow the
# buffer, causing the kernel to drop packets before they
# reach FreeRADIUS. Increasing the size of the buffer will
# avoid these packet drops.
#
# recv_buff = 65536
#
# Connection limiting for sockets with "proto = tcp".
#
# This section is ignored for other kinds of sockets.
#
limit {
#
# Limit the number of simultaneous TCP connections to the socket
#
# The default is 16.
# Setting this to 0 means "no limit"
max_connections = 16
# The per-socket "max_requests" option does not exist.
#
# The lifetime, in seconds, of a TCP connection. After
# this lifetime, the connection will be closed.
#
# Setting this to 0 means "forever".
lifetime = 0
#
# The idle timeout, in seconds, of a TCP connection.
# If no packets have been received over the connection for
# this time, the connection will be closed.
#
# Setting this to 0 means "no timeout".
#
# We STRONGLY RECOMMEND that you set an idle timeout.
#
idle_timeout = 30
}
}
#
# This second "listen" section is for listening on the accounting
# port, too.
#
listen {
ipaddr = *
# ipv6addr = ::
port = 0
type = acct
# interface = eth0
# clients = per_socket_clients
limit {
# The number of packets received can be rate limited via the
# "max_pps" configuration item. When it is set, the server
# tracks the total number of packets received in the previous
# second. If the count is greater than "max_pps", then the
# new packet is silently discarded. This helps the server
# deal with overload situations.
#
# The packets/s counter is tracked in a sliding window. This
# means that the pps calculation is done for the second
# before the current packet was received. NOT for the current
# wall-clock second, and NOT for the previous wall-clock second.
#
# Useful values are 0 (no limit), or 100 to 10000.
# Values lower than 100 will likely cause the server to ignore
# normal traffic. Few systems are capable of handling more than
# 10K packets/s.
#
# It is most useful for accounting systems. Set it to 50%
# more than the normal accounting load, and you can be sure that
# the server will never get overloaded
#
# max_pps = 0
# Only for "proto = tcp". These are ignored for "udp" sockets.
#
# idle_timeout = 0
# lifetime = 0
# max_connections = 0
}
}
# IPv6 versions of the above - read their full config to understand options
listen {
type = auth
ipv6addr = :: # any. ::1 == localhost
port = 0
# interface = eth0
# clients = per_socket_clients
limit {
max_connections = 16
lifetime = 0
idle_timeout = 30
}
}
listen {
ipv6addr = ::
port = 0
type = acct
# interface = eth0
# clients = per_socket_clients
limit {
# max_pps = 0
# idle_timeout = 0
# lifetime = 0
# max_connections = 0
}
}
# Authorization. First preprocess (hints and huntgroups files),
# then realms, and finally look in the "users" file.
#
# Any changes made here should also be made to the "inner-tunnel"
# virtual server.
#
# The order of the realm modules will determine the order that
# we try to find a matching realm.
#
# Make *sure* that 'preprocess' comes before any realm if you
# need to setup hints for the remote radius server
authorize {
#
# Take a User-Name, and perform some checks on it, for spaces and other
# invalid characters. If the User-Name appears invalid, reject the
# request.
#
# See policy.d/filter for the definition of the filter_username policy.
#
filter_username
#
# Some broken equipment sends passwords with embedded zeros.
# i.e. the debug output will show
#
# User-Password = "password\000\000"
#
# This policy will fix it to just be "password".
#
# filter_password
#
# The preprocess module takes care of sanitizing some bizarre
# attributes in the request, and turning them into attributes
# which are more standard.
#
# It takes care of processing the 'raddb/mods-config/preprocess/hints'
# and the 'raddb/mods-config/preprocess/huntgroups' files.
preprocess
# If you intend to use CUI and you require that the Operator-Name
# be set for CUI generation and you want to generate CUI also
# for your local clients then uncomment the operator-name
# below and set the operator-name for your clients in clients.conf
# operator-name
#
# If you want to generate CUI for some clients that do not
# send proper CUI requests, then uncomment the
# cui below and set "add_cui = yes" for these clients in clients.conf
# cui
#
# If you want to have a log of authentication requests,
# un-comment the following line.
# auth_log
#
# The chap module will set 'Auth-Type := CHAP' if we are
# handling a CHAP request and Auth-Type has not already been set
chap
#
# If the users are logging in with an MS-CHAP-Challenge
# attribute for authentication, the mschap module will find
# the MS-CHAP-Challenge attribute, and add 'Auth-Type := MS-CHAP'
# to the request, which will cause the server to then use
# the mschap module for authentication.
mschap
#
# If you have a Cisco SIP server authenticating against
# FreeRADIUS, uncomment the following line, and the 'digest'
# line in the 'authenticate' section.
digest
#
# The WiMAX specification says that the Calling-Station-Id
# is 6 octets of the MAC. This definition conflicts with
# RFC 3580, and all common RADIUS practices. Un-commenting
# the "wimax" module here means that it will fix the
# Calling-Station-Id attribute to the normal format as
# specified in RFC 3580 Section 3.21
# wimax
#
# Look for IPASS style 'realm/', and if not found, look for
# '@realm', and decide whether or not to proxy, based on
# that.
# IPASS
#
# Look for realms in user@domain format
suffix
# ntdomain
#
# This module takes care of EAP-MD5, EAP-TLS, and EAP-LEAP
# authentication.
#
# It also sets the EAP-Type attribute in the request
# attribute list to the EAP type from the packet.
#
# The EAP module returns "ok" or "updated" if it is not yet ready
# to authenticate the user. The configuration below checks for
# "ok", and stops processing the "authorize" section if so.
#
# Any LDAP and/or SQL servers will not be queried for the
# initial set of packets that go back and forth to set up
# TTLS or PEAP.
#
# The "updated" check is commented out for compatibility with
# previous versions of this configuration, but you may wish to
# uncomment it as well; this will further reduce the number of
# LDAP and/or SQL queries for TTLS or PEAP.
#
eap {
ok = return
# updated = return
}
#
# Pull crypt'd passwords from /etc/passwd or /etc/shadow,
# using the system API's to get the password. If you want
# to read /etc/passwd or /etc/shadow directly, see the
# mods-available/passwd module.
#
# unix
#
# Read the 'users' file. In v3, this is located in
# raddb/mods-config/files/authorize
files
#
# Look in an SQL database. The schema of the database
# is meant to mirror the "users" file.
#
# See "Authorization Queries" in mods-available/sql
-sql
#
# If you are using /etc/smbpasswd, and are also doing
# mschap authentication, the un-comment this line, and
# configure the 'smbpasswd' module.
# smbpasswd
#
# The ldap module reads passwords from the LDAP database.
-ldap
python
#
# Enforce daily limits on time spent logged in.
# daily
#
expiration
logintime
#
# If no other module has claimed responsibility for
# authentication, then try to use PAP. This allows the
# other modules listed above to add a "known good" password
# to the request, and to do nothing else. The PAP module
# will then see that password, and use it to do PAP
# authentication.
#
# This module should be listed last, so that the other modules
# get a chance to set Auth-Type for themselves.
#
pap
#
# If "status_server = yes", then Status-Server messages are passed
# through the following section, and ONLY the following section.
# This permits you to do DB queries, for example. If the modules
# listed here return "fail", then NO response is sent.
#
# Autz-Type Status-Server {
#
# }
}
# Authentication.
#
#
# This section lists which modules are available for authentication.
# Note that it does NOT mean 'try each module in order'. It means
# that a module from the 'authorize' section adds a configuration
# attribute 'Auth-Type := FOO'. That authentication type is then
# used to pick the appropriate module from the list below.
#
# In general, you SHOULD NOT set the Auth-Type attribute. The server
# will figure it out on its own, and will do the right thing. The
# most common side effect of erroneously setting the Auth-Type
# attribute is that one authentication method will work, but the
# others will not.
#
# The common reasons to set the Auth-Type attribute by hand
# is to either forcibly reject the user (Auth-Type := Reject),
# or to or forcibly accept the user (Auth-Type := Accept).
#
# Note that Auth-Type := Accept will NOT work with EAP.
#
# Please do not put "unlang" configurations into the "authenticate"
# section. Put them in the "post-auth" section instead. That's what
# the post-auth section is for.
#
authenticate {
#
# PAP authentication, when a back-end database listed
# in the 'authorize' section supplies a password. The
# password can be clear-text, or encrypted.
Auth-Type PAP {
pap
}
#
# Most people want CHAP authentication
# A back-end database listed in the 'authorize' section
# MUST supply a CLEAR TEXT password. Encrypted passwords
# won't work.
Auth-Type CHAP {
chap
}
#
# MSCHAP authentication.
Auth-Type MS-CHAP {
mschap
}
#
# For old names, too.
#
mschap
#
# If you have a Cisco SIP server authenticating against
# FreeRADIUS, uncomment the following line, and the 'digest'
# line in the 'authorize' section.
digest
#
# Pluggable Authentication Modules.
# pam
# Uncomment it if you want to use ldap for authentication
#
# Note that this means "check plain-text password against
# the ldap database", which means that EAP won't work,
# as it does not supply a plain-text password.
#
# We do NOT recommend using this. LDAP servers are databases.
# They are NOT authentication servers. FreeRADIUS is an
# authentication server, and knows what to do with authentication.
# LDAP servers do not.
#
# Auth-Type LDAP {
# ldap
# }
#
# Allow EAP authentication.
eap
#
# The older configurations sent a number of attributes in
# Access-Challenge packets, which wasn't strictly correct.
# If you want to filter out these attributes, uncomment
# the following lines.
#
# Auth-Type eap {
# eap {
# handled = 1
# }
# if (handled && (Response-Packet-Type == Access-Challenge)) {
# attr_filter.access_challenge.post-auth
# handled # override the "updated" code from attr_filter
# }
# }
}
#
# Pre-accounting. Decide which accounting type to use.
#
preacct {
preprocess
#
# Merge Acct-[Input|Output]-Gigawords and Acct-[Input-Output]-Octets
# into a single 64bit counter Acct-[Input|Output]-Octets64.
#
# acct_counters64
#
# Session start times are *implied* in RADIUS.
# The NAS never sends a "start time". Instead, it sends
# a start packet, *possibly* with an Acct-Delay-Time.
# The server is supposed to conclude that the start time
# was "Acct-Delay-Time" seconds in the past.
#
# The code below creates an explicit start time, which can
# then be used in other modules. It will be *mostly* correct.
# Any errors are due to the 1-second resolution of RADIUS,
# and the possibility that the time on the NAS may be off.
#
# The start time is: NOW - delay - session_length
#
# update request {
# &FreeRADIUS-Acct-Session-Start-Time = "%{expr: %l - %{%{Acct-Session-Time}:-0} - %{%{Acct-Delay-Time}:-0}}"
# }
#
# Ensure that we have a semi-unique identifier for every
# request, and many NAS boxes are broken.
acct_unique
#
# Look for IPASS-style 'realm/', and if not found, look for
# '@realm', and decide whether or not to proxy, based on
# that.
#
# Accounting requests are generally proxied to the same
# home server as authentication requests.
# IPASS
suffix
# ntdomain
#
# Read the 'acct_users' file
files
}
#
# Accounting. Log the accounting data.
#
accounting {
# Update accounting packet by adding the CUI attribute
# recorded from the corresponding Access-Accept
# use it only if your NAS boxes do not support CUI themselves
# cui
#
# Create a 'detail'ed log of the packets.
# Note that accounting requests which are proxied
# are also logged in the detail file.
detail
# daily
# Update the wtmp file
#
# If you don't use "radlast", you can delete this line.
unix
#
# For Simultaneous-Use tracking.
#
# Due to packet losses in the network, the data here
# may be incorrect. There is little we can do about it.
# radutmp
# sradutmp
# Return an address to the IP Pool when we see a stop record.
# main_pool
#
# Log traffic to an SQL database.
#
# See "Accounting queries" in mods-available/sql
-sql
#
# If you receive stop packets with zero session length,
# they will NOT be logged in the database. The SQL module
# will print a message (only in debugging mode), and will
# return "noop".
#
# You can ignore these packets by uncommenting the following
# three lines. Otherwise, the server will not respond to the
# accounting request, and the NAS will retransmit.
#
# if (noop) {
# ok
# }
# Cisco VoIP specific bulk accounting
# pgsql-voip
# For Exec-Program and Exec-Program-Wait
exec
# Filter attributes from the accounting response.
attr_filter.accounting_response
#
# See "Autz-Type Status-Server" for how this works.
#
# Acct-Type Status-Server {
#
# }
}
# Session database, used for checking Simultaneous-Use. Either the radutmp
# or rlm_sql module can handle this.
# The rlm_sql module is *much* faster
session {
# radutmp
#
# See "Simultaneous Use Checking Queries" in mods-available/sql
# sql
}
# Post-Authentication
# Once we KNOW that the user has been authenticated, there are
# additional steps we can take.
post-auth {
#
# If you need to have a State attribute, you can
# add it here. e.g. for later CoA-Request with
# State, and Service-Type = Authorize-Only.
#
# if (!&reply:State) {
# update reply {
# State := "0x%{randstr:16h}"
# }
# }
#
# For EAP-TTLS and PEAP, add the cached attributes to the reply.
# The "session-state" attributes are automatically cached when
# an Access-Challenge is sent, and automatically retrieved
# when an Access-Request is received.
#
# The session-state attributes are automatically deleted after
# an Access-Reject or Access-Accept is sent.
#
# If both session-state and reply contain a User-Name attribute, remove
# the one in the reply if it is just a copy of the one in the request, so
# we don't end up with two User-Name attributes.
if (session-state:User-Name && reply:User-Name && request:User-Name && (reply:User-Name == request:User-Name)) {
update reply {
&User-Name !* ANY
}
}
update {
&reply: += &session-state:
}
# Get an address from the IP Pool.
# main_pool
# Create the CUI value and add the attribute to Access-Accept.
# Uncomment the line below if *returning* the CUI.
# cui
# Create empty accounting session to make simultaneous check
# more robust. See the accounting queries configuration in
# raddb/mods-config/sql/main/*/queries.conf for details.
#
# The "sql_session_start" policy is defined in
# raddb/policy.d/accounting. See that file for more details.
# sql_session_start
#
# If you want to have a log of authentication replies,
# un-comment the following line, and enable the
# 'detail reply_log' module.
# reply_log
#
# After authenticating the user, do another SQL query.
#
# See "Authentication Logging Queries" in mods-available/sql
-sql
#
# Un-comment the following if you want to modify the user's object
# in LDAP after a successful login.
#
# ldap
# For Exec-Program and Exec-Program-Wait
exec
#
# Calculate the various WiMAX keys. In order for this to work,
# you will need to define the WiMAX NAI, usually via
#
# update request {
# WiMAX-MN-NAI = "%{User-Name}"
# }
#
# If you want various keys to be calculated, you will need to
# update the reply with "template" values. The module will see
# this, and replace the template values with the correct ones
# taken from the cryptographic calculations. e.g.
#
# update reply {
# WiMAX-FA-RK-Key = 0x00
# WiMAX-MSK = "%{EAP-MSK}"
# }
#
# You may want to delete the MS-MPPE-*-Keys from the reply,
# as some WiMAX clients behave badly when those attributes
# are included. See "raddb/modules/wimax", configuration
# entry "delete_mppe_keys" for more information.
#
# wimax
# If there is a client certificate (EAP-TLS, sometimes PEAP
# and TTLS), then some attributes are filled out after the
# certificate verification has been performed. These fields
# MAY be available during the authentication, or they may be
# available only in the "post-auth" section.
#
# The first set of attributes contains information about the
# issuing certificate which is being used. The second
# contains information about the client certificate (if
# available).
#
# update reply {
# Reply-Message += "%{TLS-Cert-Serial}"
# Reply-Message += "%{TLS-Cert-Expiration}"
# Reply-Message += "%{TLS-Cert-Subject}"
# Reply-Message += "%{TLS-Cert-Issuer}"
# Reply-Message += "%{TLS-Cert-Common-Name}"
# Reply-Message += "%{TLS-Cert-Subject-Alt-Name-Email}"
#
# Reply-Message += "%{TLS-Client-Cert-Serial}"
# Reply-Message += "%{TLS-Client-Cert-Expiration}"
# Reply-Message += "%{TLS-Client-Cert-Subject}"
# Reply-Message += "%{TLS-Client-Cert-Issuer}"
# Reply-Message += "%{TLS-Client-Cert-Common-Name}"
# Reply-Message += "%{TLS-Client-Cert-Subject-Alt-Name-Email}"
# }
# Insert class attribute (with unique value) into response,
# aids matching auth and acct records, and protects against duplicate
# Acct-Session-Id. Note: Only works if the NAS has implemented
# RFC 2865 behaviour for the class attribute, AND if the NAS
# supports long Class attributes. Many older or cheap NASes
# only support 16-octet Class attributes.
# insert_acct_class
# MacSEC requires the use of EAP-Key-Name. However, we don't
# want to send it for all EAP sessions. Therefore, the EAP
# modules put required data into the EAP-Session-Id attribute.
# This attribute is never put into a request or reply packet.
#
# Uncomment the next few lines to copy the required data into
# the EAP-Key-Name attribute
# if (&reply:EAP-Session-Id) {
# update reply {
# EAP-Key-Name := &reply:EAP-Session-Id
# }
# }
# Remove reply message if the response contains an EAP-Message
remove_reply_message_if_eap
#
# Access-Reject packets are sent through the REJECT sub-section of the
# post-auth section.
#
# Add the ldap module name (or instance) if you have set
# 'edir_account_policy_check = yes' in the ldap module configuration
#
# The "session-state" attributes are not available here.
#
Post-Auth-Type REJECT {
# log failed authentications in SQL, too.
-sql
attr_filter.access_reject
# Insert EAP-Failure message if the request was
# rejected by policy instead of because of an
# authentication failure
eap
# Remove reply message if the response contains an EAP-Message
remove_reply_message_if_eap
}
#
# Filter access challenges.
#
Post-Auth-Type Challenge {
# remove_reply_message_if_eap
# attr_filter.access_challenge.post-auth
}
}
#
# When the server decides to proxy a request to a home server,
# the proxied request is first passed through the pre-proxy
# stage. This stage can re-write the request, or decide to
# cancel the proxy.
#
# Only a few modules currently have this method.
#
pre-proxy {
# Before proxing the request add an Operator-Name attribute identifying
# if the operator-name is found for this client.
# No need to uncomment this if you have already enabled this in
# the authorize section.
# operator-name
# The client requests the CUI by sending a CUI attribute
# containing one zero byte.
# Uncomment the line below if *requesting* the CUI.
# cui
# Uncomment the following line if you want to change attributes
# as defined in the preproxy_users file.
# files
# Uncomment the following line if you want to filter requests
# sent to remote servers based on the rules defined in the
# 'attrs.pre-proxy' file.
# attr_filter.pre-proxy
# If you want to have a log of packets proxied to a home
# server, un-comment the following line, and the
# 'detail pre_proxy_log' section, above.
# pre_proxy_log
}
#
# When the server receives a reply to a request it proxied
# to a home server, the request may be massaged here, in the
# post-proxy stage.
#
post-proxy {
# If you want to have a log of replies from a home server,
# un-comment the following line, and the 'detail post_proxy_log'
# section, above.
# post_proxy_log
# Uncomment the following line if you want to filter replies from
# remote proxies based on the rules defined in the 'attrs' file.
# attr_filter.post-proxy
#
# If you are proxying LEAP, you MUST configure the EAP
# module, and you MUST list it here, in the post-proxy
# stage.
#
# You MUST also use the 'nostrip' option in the 'realm'
# configuration. Otherwise, the User-Name attribute
# in the proxied request will not match the user name
# hidden inside of the EAP packet, and the end server will
# reject the EAP request.
#
eap
#
# If the server tries to proxy a request and fails, then the
# request is processed through the modules in this section.
#
# The main use of this section is to permit robust proxying
# of accounting packets. The server can be configured to
# proxy accounting packets as part of normal processing.
# Then, if the home server goes down, accounting packets can
# be logged to a local "detail" file, for processing with
# radrelay. When the home server comes back up, radrelay
# will read the detail file, and send the packets to the
# home server.
#
# With this configuration, the server always responds to
# Accounting-Requests from the NAS, but only writes
# accounting packets to disk if the home server is down.
#
# Post-Proxy-Type Fail-Accounting {
# detail
# }
}
}

View file

@ -0,0 +1,94 @@
import sys
import os
import subprocess
import atexit
import shutil
import signal
MAJOR, MINOR, _, _, _ = sys.version_info
if MAJOR >= 3:
import configparser
else:
import ConfigParser as configparser
DEBUG = True
CONFIG = configparser.ConfigParser()
CONFIG.read('/data/config.ini')
CLIENTS = [
{
"name": x.split('.')[1],
"secret": CONFIG.get(x, "secret"),
"ipaddr": CONFIG.get(x, "ipaddr"),
}
for x in CONFIG.sections()
if x.startswith('client.')
]
print(CLIENTS)
def _sigchild_handler(*args, **kwargs):
# log.debug("Received SIGCHLD ...")
os.waitpid(-1, os.WNOHANG)
def write_clients_conf():
with open('/etc/raddb/clients.conf', 'w') as f:
for client in CLIENTS:
f.write('client %s {\n' % client['name'])
f.write(' ipaddr = %s\n' % client['ipaddr'])
f.write(' secret = %s\n' % client['secret'])
f.write(' proto = *\n')
f.write('}\n')
def setup_certs():
# copy ca to /etc/raddb/certs/ca.pem
shutil.copyfile(CONFIG.get("radiusd", "ca"), '/etc/raddb/certs/ca.pem')
shutil.copyfile(CONFIG.get("radiusd", "dh"), '/etc/raddb/certs/dh')
# concat key + cert into /etc/raddb/certs/server.pem
with open('/etc/raddb/certs/server.pem', 'w') as f:
with open(CONFIG.get("radiusd", "key"), 'r') as r:
f.write(r.read())
f.write('\n')
with open(CONFIG.get("radiusd", "cert"), 'r') as r:
f.write(r.read())
def run_radiusd():
global proc
if DEBUG:
proc = subprocess.Popen([
"/usr/sbin/radiusd", "-X"
], stderr=subprocess.STDOUT)
else:
proc = subprocess.Popen([
"/usr/sbin/radiusd", "-f",
"-l", "stdout"
], stderr=subprocess.STDOUT)
print(proc)
def kill_radius():
if proc is None:
pass
else:
try:
os.kill(proc.pid, signal.SIGTERM)
except:
# It's already gone ...
pass
print("Stopping radiusd ...")
# To make sure we really do shutdown, we actually re-block on the proc
# again here to be sure it's done.
proc.wait()
atexit.register(kill_radius)
proc.wait()
if __name__ == '__main__':
signal.signal(signal.SIGCHLD, _sigchild_handler)
setup_certs()
write_clients_conf()
run_radiusd()

View file

@ -0,0 +1,440 @@
# -*- text -*-
######################################################################
#
# This is a virtual server that handles *only* inner tunnel
# requests for EAP-TTLS and PEAP types.
#
# $Id: 8bc463d0bbfe43ea8b36a4992000fe83b14f7d1a $
#
######################################################################
server inner-tunnel {
#
# This next section is here to allow testing of the "inner-tunnel"
# authentication methods, independently from the "default" server.
# It is listening on "localhost", so that it can only be used from
# the same machine.
#
# $ radtest USER PASSWORD 127.0.0.1:18120 0 testing123
#
# If it works, you have configured the inner tunnel correctly. To check
# if PEAP will work, use:
#
# $ radtest -t mschap USER PASSWORD 127.0.0.1:18120 0 testing123
#
# If that works, PEAP should work. If that command doesn't work, then
#
# FIX THE INNER TUNNEL CONFIGURATION SO THAT IT WORKS.
#
# Do NOT do any PEAP tests. It won't help. Instead, concentrate
# on fixing the inner tunnel configuration. DO NOTHING ELSE.
#
listen {
ipaddr = 127.0.0.1
port = 18120
type = auth
}
# Authorization. First preprocess (hints and huntgroups files),
# then realms, and finally look in the "users" file.
#
# The order of the realm modules will determine the order that
# we try to find a matching realm.
#
# Make *sure* that 'preprocess' comes before any realm if you
# need to setup hints for the remote radius server
authorize {
#
# Take a User-Name, and perform some checks on it, for spaces and other
# invalid characters. If the User-Name appears invalid, reject the
# request.
#
# See policy.d/filter for the definition of the filter_username policy.
#
filter_username
#
# Do checks on outer / inner User-Name, so that users
# can't spoof us by using incompatible identities
#
# filter_inner_identity
#
# The chap module will set 'Auth-Type := CHAP' if we are
# handling a CHAP request and Auth-Type has not already been set
chap
#
# If the users are logging in with an MS-CHAP-Challenge
# attribute for authentication, the mschap module will find
# the MS-CHAP-Challenge attribute, and add 'Auth-Type := MS-CHAP'
# to the request, which will cause the server to then use
# the mschap module for authentication.
mschap
#
# Pull crypt'd passwords from /etc/passwd or /etc/shadow,
# using the system API's to get the password. If you want
# to read /etc/passwd or /etc/shadow directly, see the
# passwd module, above.
#
# unix
#
# Look for IPASS style 'realm/', and if not found, look for
# '@realm', and decide whether or not to proxy, based on
# that.
# IPASS
#
# Look for realms in user@domain format
#
# Note that proxying the inner tunnel authentication means
# that the user MAY use one identity in the outer session
# (e.g. "anonymous", and a different one here
# (e.g. "user@example.com"). The inner session will then be
# proxied elsewhere for authentication. If you are not
# careful, this means that the user can cause you to forward
# the authentication to another RADIUS server, and have the
# accounting logs *not* sent to the other server. This makes
# it difficult to bill people for their network activity.
#
suffix
# ntdomain
#
# The "suffix" module takes care of stripping the domain
# (e.g. "@example.com") from the User-Name attribute, and the
# next few lines ensure that the request is not proxied.
#
# If you want the inner tunnel request to be proxied, delete
# the next few lines.
#
update control {
&Proxy-To-Realm := LOCAL
}
#
# This module takes care of EAP-MSCHAPv2 authentication.
#
# It also sets the EAP-Type attribute in the request
# attribute list to the EAP type from the packet.
#
# The example below uses module failover to avoid querying all
# of the following modules if the EAP module returns "ok".
# Therefore, your LDAP and/or SQL servers will not be queried
# for the many packets that go back and forth to set up TTLS
# or PEAP. The load on those servers will therefore be reduced.
#
eap {
ok = return
}
#
# Read the 'users' file
files
#
# Look in an SQL database. The schema of the database
# is meant to mirror the "users" file.
#
# See "Authorization Queries" in sql.conf
-sql
#
# If you are using /etc/smbpasswd, and are also doing
# mschap authentication, the un-comment this line, and
# enable the "smbpasswd" module.
# smbpasswd
#
# The ldap module reads passwords from the LDAP database.
-ldap
python
#
# Enforce daily limits on time spent logged in.
# daily
expiration
logintime
#
# If no other module has claimed responsibility for
# authentication, then try to use PAP. This allows the
# other modules listed above to add a "known good" password
# to the request, and to do nothing else. The PAP module
# will then see that password, and use it to do PAP
# authentication.
#
# This module should be listed last, so that the other modules
# get a chance to set Auth-Type for themselves.
#
pap
}
# Authentication.
#
#
# This section lists which modules are available for authentication.
# Note that it does NOT mean 'try each module in order'. It means
# that a module from the 'authorize' section adds a configuration
# attribute 'Auth-Type := FOO'. That authentication type is then
# used to pick the appropriate module from the list below.
#
# In general, you SHOULD NOT set the Auth-Type attribute. The server
# will figure it out on its own, and will do the right thing. The
# most common side effect of erroneously setting the Auth-Type
# attribute is that one authentication method will work, but the
# others will not.
#
# The common reasons to set the Auth-Type attribute by hand
# is to either forcibly reject the user, or forcibly accept him.
#
authenticate {
#
# PAP authentication, when a back-end database listed
# in the 'authorize' section supplies a password. The
# password can be clear-text, or encrypted.
Auth-Type PAP {
pap
}
#
# Most people want CHAP authentication
# A back-end database listed in the 'authorize' section
# MUST supply a CLEAR TEXT password. Encrypted passwords
# won't work.
Auth-Type CHAP {
chap
}
#
# MSCHAP authentication.
Auth-Type MS-CHAP {
mschap
}
#
# For old names, too.
#
mschap
#
# Pluggable Authentication Modules.
# pam
# Uncomment it if you want to use ldap for authentication
#
# Note that this means "check plain-text password against
# the ldap database", which means that EAP won't work,
# as it does not supply a plain-text password.
#
# We do NOT recommend using this. LDAP servers are databases.
# They are NOT authentication servers. FreeRADIUS is an
# authentication server, and knows what to do with authentication.
# LDAP servers do not.
#
# Auth-Type LDAP {
# ldap
# }
#
# Allow EAP authentication.
eap
}
######################################################################
#
# There are no accounting requests inside of EAP-TTLS or PEAP
# tunnels.
#
######################################################################
# Session database, used for checking Simultaneous-Use. Either the radutmp
# or rlm_sql module can handle this.
# The rlm_sql module is *much* faster
session {
radutmp
#
# See "Simultaneous Use Checking Queries" in sql.conf
# sql
}
# Post-Authentication
# Once we KNOW that the user has been authenticated, there are
# additional steps we can take.
#
# Note that the last packet of the inner-tunnel authentication
# MAY NOT BE the last packet of the outer session. So updating
# the outer reply MIGHT work, and sometimes MIGHT NOT. The
# exact functionality depends on both the inner and outer
# authentication methods.
#
# If you need to send a reply attribute in the outer session,
# the ONLY safe way is to set "use_tunneled_reply = yes", and
# then update the inner-tunnel reply.
post-auth {
# If you want privacy to remain, see the
# Chargeable-User-Identity attribute from RFC 4372.
# If you want to use it just uncomment the line below.
# cui-inner
#
# If you want the Access-Accept to contain the inner
# User-Name, uncomment the following lines.
#
# update outer.session-state {
# User-Name := &User-Name
# }
#
# If you want to have a log of authentication replies,
# un-comment the following line, and enable the
# 'detail reply_log' module.
# reply_log
#
# After authenticating the user, do another SQL query.
#
# See "Authentication Logging Queries" in sql.conf
-sql
#
# Un-comment the following if you have set
# 'edir_account_policy_check = yes' in the ldap module sub-section of
# the 'modules' section.
#
# ldap
#
# Un-comment the following if you want to generate Moonshot (ABFAB) TargetedIds
#
# IMPORTANT: This requires the UUID package to be installed, and a targeted_id_salt
# to be configured.
#
# This functionality also supports SQL backing. To use this functionality, enable
# and configure the moonshot-targeted-ids SQL module in the mods-enabled directory.
# Then remove the comments from the appropriate lines in each of the below
# policies in the policy.d/moonshot-targeted-ids file.
#
# moonshot_host_tid
# moonshot_realm_tid
# moonshot_coi_tid
#
# Instead of "use_tunneled_reply", change this "if (0)" to an
# "if (1)".
#
if (0) {
#
# These attributes are for the inner-tunnel only,
# and MUST NOT be copied to the outer reply.
#
update reply {
User-Name !* ANY
Message-Authenticator !* ANY
EAP-Message !* ANY
Proxy-State !* ANY
MS-MPPE-Encryption-Types !* ANY
MS-MPPE-Encryption-Policy !* ANY
MS-MPPE-Send-Key !* ANY
MS-MPPE-Recv-Key !* ANY
}
#
# Copy the inner reply attributes to the outer
# session-state list. The post-auth policy will take
# care of copying the outer session-state list to the
# outer reply.
#
update {
&outer.session-state: += &reply:
}
}
#
# Access-Reject packets are sent through the REJECT sub-section of the
# post-auth section.
#
# Add the ldap module name (or instance) if you have set
# 'edir_account_policy_check = yes' in the ldap module configuration
#
Post-Auth-Type REJECT {
# log failed authentications in SQL, too.
-sql
attr_filter.access_reject
#
# Let the outer session know which module failed, and why.
#
update outer.session-state {
&Module-Failure-Message := &request:Module-Failure-Message
}
}
}
#
# When the server decides to proxy a request to a home server,
# the proxied request is first passed through the pre-proxy
# stage. This stage can re-write the request, or decide to
# cancel the proxy.
#
# Only a few modules currently have this method.
#
pre-proxy {
# Uncomment the following line if you want to change attributes
# as defined in the preproxy_users file.
# files
# Uncomment the following line if you want to filter requests
# sent to remote servers based on the rules defined in the
# 'attrs.pre-proxy' file.
# attr_filter.pre-proxy
# If you want to have a log of packets proxied to a home
# server, un-comment the following line, and the
# 'detail pre_proxy_log' section, above.
# pre_proxy_log
}
#
# When the server receives a reply to a request it proxied
# to a home server, the request may be massaged here, in the
# post-proxy stage.
#
post-proxy {
# If you want to have a log of replies from a home server,
# un-comment the following line, and the 'detail post_proxy_log'
# section, above.
# post_proxy_log
# Uncomment the following line if you want to filter replies from
# remote proxies based on the rules defined in the 'attrs' file.
# attr_filter.post-proxy
#
# If you are proxying LEAP, you MUST configure the EAP
# module, and you MUST list it here, in the post-proxy
# stage.
#
# You MUST also use the 'nostrip' option in the 'realm'
# configuration. Otherwise, the User-Name attribute
# in the proxied request will not match the user name
# hidden inside of the EAP packet, and the end server will
# reject the EAP request.
#
eap
}
} # inner-tunnel server block

View file

@ -0,0 +1,138 @@
import sys
import requests
import logging
import os
MAJOR, MINOR, _, _, _ = sys.version_info
if MAJOR >= 3:
import configparser
else:
import ConfigParser as configparser
# Setup the config too
print(os.getcwd())
CONFIG = configparser.ConfigParser()
CONFIG.read('/data/config.ini')
GROUPS = [
{
"name": x.split('.')[1],
"vlan": CONFIG.get(x, "vlan")
}
for x in CONFIG.sections()
if x.startswith('group.')
]
REQ_GROUP = CONFIG.get("radiusd", "required_group")
if CONFIG.getboolean("kanidm_client", "strict"):
CA = CONFIG.get("kanidm_client", "ca")
else:
CA = False
USER = CONFIG.get("kanidm_client", "user")
SECRET = CONFIG.get("kanidm_client", "secret")
URL = CONFIG.get('kanidm_client', 'url')
AUTH_URL = "%s/v1/auth" % URL
def _authenticate(s, acct, pw):
init_auth = {"step": { "Init": [acct, None]}}
r = s.post(AUTH_URL, json=init_auth, verify=CA)
if r.status_code != 200:
print(r.json())
raise Exception("AuthInitFailed")
cred_auth = {"step": { "Creds": [{"Password": pw}]}}
r = s.post(AUTH_URL, json=cred_auth, verify=CA)
if r.status_code != 200:
print(r.json())
raise Exception("AuthCredFailed")
def _get_radius_token(username):
print("getting rtok for %s ..." % username)
s = requests.session()
# First authenticate a connection
_authenticate(s, USER, SECRET)
# Now get the radius token
rtok_url = "%s/v1/account/%s/_radius/_token" % (URL, username)
r = s.get(rtok_url)
if r.status_code != 200:
raise Exception("Failed to get RadiusAuthToken")
tok = r.json()
return(tok)
def check_vlan(acc, group):
if CONFIG.has_section("group.%s" % group['name']):
if CONFIG.has_option("group.%s" % group['name'], "vlan"):
v = CONFIG.get("group.%s" % group['name'], "vlan")
print("assigning vlan %s from %s" % (v,group))
return v
return acc
def instantiate(args):
print(args)
return radiusd.RLM_MODULE_OK
def authorize(args):
radiusd.radlog(radiusd.L_INFO, 'kanidm python module called')
dargs = dict(args)
print(dargs)
username = dargs['User-Name']
try:
tok = _get_radius_token(username)
except Exception as e:
print(e)
return radiusd.RLM_MODULE_NOTFOUND
print("got token %s" % tok)
# Are they in the required group?
req_sat = False
for group in tok["groups"]:
if group['name'] == REQ_GROUP:
req_sat = True
print("required group satisfied -> %s" % req_sat)
if req_sat is not True:
return radiusd.RLM_MODULE_NOTFOUND
# look up them in config for group vlan if possible.
uservlan = reduce(check_vlan, tok["groups"], 0)
print("selected vlan %s" % uservlan)
# Convert the tok groups to groups.
name = tok["name"]
secret = tok["secret"]
reply = (
('User-Name', str(name)),
('Reply-Message', 'Welcome'),
('Tunnel-Type', '13'),
('Tunnel-Medium-Type', '6'),
('Tunnel-Private-Group-ID', str(uservlan)),
)
config = (
('Cleartext-Password', str(secret)),
)
print("OK! Returning details to radius ...")
return (radiusd.RLM_MODULE_OK, reply, config)
if __name__ == '__main__':
# Test getting from the kanidm server instead.
if len(sys.argv) != 2:
print("usage: %s username" % sys.argv[0])
else:
tok = _get_radius_token(sys.argv[1])
print(tok)
print(tok["groups"])
else:
import radiusd

View file

@ -0,0 +1,65 @@
#
# Make sure the PYTHONPATH environmental variable contains the
# directory(s) for the modules listed below.
#
# Uncomment any func_* which are included in your module. If
# rlm_python is called for a section which does not have
# a function defined, it will return NOOP.
#
python {
# Path to the python modules
#
# Note that due to limitations on Python, this configuration
# item is GLOBAL TO THE SERVER. That is, you cannot have two
# instances of the python module, each with a different path.
#
python_path="/usr/lib64/python2.7:/usr/lib/python2.7:/usr/lib/python2.7/site-packages:/usr/lib64/python2.7/site-packages:/usr/lib64/python2.7/lib-dynload:/etc/raddb"
module = kanidmradius
# Pass all VPS lists as a 6-tuple to the callbacks
# (request, reply, config, state, proxy_req, proxy_reply)
# pass_all_vps = no
# Pass all VPS lists as a dictionary to the callbacks
# Keys: "request", "reply", "config", "session-state", "proxy-request",
# "proxy-reply"
# This option prevales over "pass_all_vps"
# pass_all_vps_dict = no
mod_instantiate = ${.module}
func_instantiate = instantiate
mod_detach = ${.module}
# func_detach = detach
mod_authorize = ${.module}
func_authorize = authorize
mod_authenticate = ${.module}
# func_authenticate = authenticate
mod_preacct = ${.module}
# func_preacct = preacct
mod_accounting = ${.module}
# func_accounting = accounting
mod_checksimul = ${.module}
# func_checksimul = checksimul
mod_pre_proxy = ${.module}
# func_pre_proxy = pre_proxy
mod_post_proxy = ${.module}
# func_post_proxy = post_proxy
mod_post_auth = ${.module}
# func_post_auth = post_auth
mod_recv_coa = ${.module}
# func_recv_coa = recv_coa
mod_send_coa = ${.module}
# func_send_coa = send_coa
}

View file

@ -0,0 +1,29 @@
[kanidm_client]
url = https://172.17.0.3:8080
ca = /data/ca.pem
strict = false
user = radius_server
secret = DYDVFqk1auAhFWrD1wzJnYBrKGFhwlxEQc2t5JK0vx3sTymj
; default vlans for groups that don't specify one.
[DEFAULT]
vlan = 10
[group.idm_admins]
vlan = 12
[radiusd]
ca = /data/certs/ca.pem
key = /data/certs/key.pem
cert = /data/certs/cert.pem
dh = /data/certs/dh
required_group = idm_admins
[client.localhost]
ipaddr = 127.0.0.1
secret = testing123
[client.docker]
ipaddr = 172.17.0.0/24
secret = docker123

View file

@ -102,6 +102,14 @@ struct AccountCredentialSet {
copt: CommonOpt,
}
#[derive(Debug, StructOpt)]
struct AccountRadiusOpt {
#[structopt(flatten)]
aopts: AccountCommonOpt,
#[structopt(flatten)]
copt: CommonOpt,
}
#[derive(Debug, StructOpt)]
enum AccountCredential {
#[structopt(name = "set_password")]
@ -110,10 +118,22 @@ enum AccountCredential {
GeneratePassword(AccountCredentialSet),
}
#[derive(Debug, StructOpt)]
enum AccountRadius {
#[structopt(name = "show_secret")]
Show(AccountRadiusOpt),
#[structopt(name = "generate_secret")]
Generate(AccountRadiusOpt),
#[structopt(name = "delete_secret")]
Delete(AccountRadiusOpt),
}
#[derive(Debug, StructOpt)]
enum AccountOpt {
#[structopt(name = "credential")]
Credential(AccountCredential),
#[structopt(name = "radius")]
Radius(AccountRadius),
}
#[derive(Debug, StructOpt)]
@ -274,6 +294,32 @@ fn main() {
);
}
}, // end AccountOpt::Credential
AccountOpt::Radius(aropt) => match aropt {
AccountRadius::Show(aopt) => {
let client = aopt.copt.to_client();
let rcred = client
.idm_account_radius_credential_get(aopt.aopts.account_id.as_str())
.unwrap();
match rcred {
Some(s) => println!("Radius secret: {}", s),
None => println!("NO Radius secret"),
}
}
AccountRadius::Generate(aopt) => {
let client = aopt.copt.to_client();
client
.idm_account_radius_credential_regenerate(aopt.aopts.account_id.as_str())
.unwrap();
}
AccountRadius::Delete(aopt) => {
let client = aopt.copt.to_client();
client
.idm_account_radius_credential_delete(aopt.aopts.account_id.as_str())
.unwrap();
}
}, // end AccountOpt::Radius
},
}
}

View file

@ -18,5 +18,5 @@ RUN cd /etc && \
VOLUME /data
ENV RUST_BACKTRACE 1
CMD ["/home/kanidm/target/release/kanidmd", "server", "-D", "/data/kanidm.db"]
CMD ["/home/kanidm/target/release/kanidmd", "server", "-D", "/data/kanidm.db", "-C", "/data/ca.pem", "-c", "/data/cert.pem", "-k", "/data/key.pem", "--bindaddr", "0.0.0.0:8080"]

View file

@ -46,7 +46,7 @@ lazy_static! {
pub struct AccessControlSearch {
acp: AccessControlProfile,
// TODO: Should this change to Value? May help to reduce transformations during processing.
attrs: Vec<String>,
attrs: BTreeSet<String>,
}
impl AccessControlSearch {
@ -65,7 +65,7 @@ impl AccessControlSearch {
let attrs = try_audit!(
audit,
value
.get_ava_string("acp_search_attr")
.get_ava_set_string("acp_search_attr")
.ok_or(OperationError::InvalidACPState(
"Missing acp_search_attr".to_string()
))
@ -521,10 +521,21 @@ pub trait AccessControlsTransaction {
// interface is beyond me ....
let rec_entry: &Entry<EntryValid, EntryCommitted> = match &se.event.origin {
EventOrigin::Internal => {
if cfg!(test) {
audit_log!(audit, "TEST: Internal search in external interface - allowing due to cfg test ...");
// In tests we just push everything back.
return Ok(entries
.into_iter()
.map(|e| unsafe { e.to_reduced() })
.collect());
} else {
// In production we can't risk leaking data here, so we return
// empty sets.
audit_log!(audit, "IMPOSSIBLE STATE: Internal search in external interface?! Returning empty for safety.");
// No need to check ACS
return Ok(Vec::new());
}
}
EventOrigin::User(e) => &e,
};
@ -539,8 +550,26 @@ pub trait AccessControlsTransaction {
let f_val = acs.acp.receiver.clone();
match f_val.resolve(&se.event, None) {
Ok(f_res) => {
// Is our user covered by this acs?
if rec_entry.entry_match_no_index(&f_res) {
// If so, let's check if the attr request is relevant.
// If we have a requested attr set, are any of them
// in the attrs this acs covers?
let acs_target_attrs = match &se.attrs {
Some(r_attrs) => acs.attrs.intersection(r_attrs).count(),
// All attrs requested, do nothing.
None => acs.attrs.len(),
};
// There is nothing in the ACS (not possible) or
// no overlap between the requested set and this acs, so it's
// not worth evaling.
if acs_target_attrs == 0 {
None
} else {
Some(acs)
}
} else {
None
}
@ -561,10 +590,11 @@ pub trait AccessControlsTransaction {
audit_log!(audit, "Related acs -> {:?}", racp.acp.name);
});
// Get the set of attributes requested by the caller
// TODO #69: This currently
// is ALL ATTRIBUTES, so we actually work here to just remove things we
// CAN'T see instead.
// Build a reference set from the req_attrs
let req_attrs: Option<BTreeSet<_>> = se
.attrs
.as_ref()
.map(|vs| vs.iter().map(|s| s.as_str()).collect());
// For each entry
let allowed_entries: Vec<Entry<EntryReduced, EntryCommitted>> = entries
@ -611,12 +641,20 @@ pub trait AccessControlsTransaction {
})
.flatten()
.collect();
// Remove all others that are present on the entry.
audit_log!(audit, "-- for entry --> {:?}", e.get_uuid());
audit_log!(audit, "requested attributes --> {:?}", req_attrs);
audit_log!(audit, "allowed attributes --> {:?}", allowed_attrs);
// Remove anything that wasn't requested.
let f_allowed_attrs: BTreeSet<&str> = match &req_attrs {
Some(v) => allowed_attrs.intersection(&v).map(|s| *s).collect(),
None => allowed_attrs,
};
// Now purge the attrs that are NOT in this.
e.reduce_attributes(allowed_attrs)
e.reduce_attributes(f_allowed_attrs)
})
.collect();
Ok(allowed_entries)
@ -1789,8 +1827,10 @@ mod tests {
.expect("operation failed");
// Help the type checker for the expect set.
let expect_set: Vec<Entry<EntryReduced, EntryCommitted>> =
$expect.into_iter().map(|e| e.to_reduced()).collect();
let expect_set: Vec<Entry<EntryReduced, EntryCommitted>> = $expect
.into_iter()
.map(|e| unsafe { e.to_reduced() })
.collect();
println!("expect --> {:?}", expect_set);
println!("result --> {:?}", reduced);
@ -1846,6 +1886,47 @@ mod tests {
test_acp_search_reduce!(&se_anon, vec![acp], r_set, ex_anon);
}
#[test]
fn test_access_enforce_search_attrs_req() {
// Test that attributes are correctly limited by the request.
// In this case, we test that a user can only see "name" despite the
// class and uuid being present.
let e1: Entry<EntryInvalid, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1);
let ev1 = unsafe { e1.to_valid_committed() };
let r_set = vec![ev1.clone()];
let ex1: Entry<EntryInvalid, EntryNew> =
Entry::unsafe_from_entry_str(JSON_TESTPERSON1_REDUCED);
let exv1 = unsafe { ex1.to_valid_committed() };
let ex_anon = vec![exv1.clone()];
let mut se_anon = unsafe {
SearchEvent::new_impersonate_entry_ser(
JSON_ANONYMOUS_V1,
filter_all!(f_eq("name", PartialValue::new_iutf8s("testperson1"))),
)
};
// the requested attrs here.
se_anon.attrs = Some(btreeset!["name".to_string()]);
let acp = unsafe {
AccessControlSearch::from_raw(
"test_acp",
"d38640c4-0254-49f9-99b7-8ba7d0233f3d",
// apply to anonymous only
filter_valid!(f_eq("name", PartialValue::new_iutf8s("anonymous"))),
// Allow anonymous to read only testperson1
filter_valid!(f_eq("name", PartialValue::new_iutf8s("testperson1"))),
// In that read, admin may only view the "name" attribute, or query on
// the name attribute. Any other query (should be) rejected.
"name uuid",
)
};
// Finally test it!
test_acp_search_reduce!(&se_anon, vec![acp], r_set, ex_anon);
}
macro_rules! test_acp_modify {
(
$me:expr,

View file

@ -4,7 +4,8 @@ use crate::audit::AuditScope;
use crate::async_log::EventLog;
use crate::event::{AuthEvent, SearchEvent, SearchResult, WhoamiResult};
use kanidm_proto::v1::OperationError;
use crate::idm::event::RadiusAuthTokenEvent;
use kanidm_proto::v1::{OperationError, RadiusAuthToken};
use crate::filter::{Filter, FilterInvalid};
use crate::idm::server::IdmServer;
@ -77,21 +78,31 @@ impl Message for SearchMessage {
pub struct InternalSearchMessage {
pub uat: Option<UserAuthToken>,
pub filter: Filter<FilterInvalid>,
}
impl InternalSearchMessage {
pub fn new(uat: Option<UserAuthToken>, filter: Filter<FilterInvalid>) -> Self {
InternalSearchMessage {
uat: uat,
filter: filter,
}
}
pub attrs: Option<Vec<String>>,
}
impl Message for InternalSearchMessage {
type Result = Result<Vec<ProtoEntry>, OperationError>;
}
pub struct InternalRadiusReadMessage {
pub uat: Option<UserAuthToken>,
pub uuid_or_name: String,
}
impl Message for InternalRadiusReadMessage {
type Result = Result<Option<String>, OperationError>;
}
pub struct InternalRadiusTokenReadMessage {
pub uat: Option<UserAuthToken>,
pub uuid_or_name: String,
}
impl Message for InternalRadiusTokenReadMessage {
type Result = Result<RadiusAuthToken, OperationError>;
}
// ===========================================================
pub struct QueryServerReadV1 {
@ -297,3 +308,103 @@ impl Handler<InternalSearchMessage> for QueryServerReadV1 {
res
}
}
impl Handler<InternalRadiusReadMessage> for QueryServerReadV1 {
type Result = Result<Option<String>, OperationError>;
fn handle(&mut self, msg: InternalRadiusReadMessage, _: &mut Self::Context) -> Self::Result {
let mut audit = AuditScope::new("internal_radius_read_message");
let res = audit_segment!(&mut audit, || {
let qs_read = self.qs.read();
let target_uuid = match Uuid::parse_str(msg.uuid_or_name.as_str()) {
Ok(u) => u,
Err(_) => qs_read
.name_to_uuid(&mut audit, msg.uuid_or_name.as_str())
.map_err(|e| {
audit_log!(&mut audit, "Error resolving id to target");
e
})?,
};
// Make an event from the request
let srch = match SearchEvent::from_target_uuid_request(
&mut audit,
msg.uat,
target_uuid,
&qs_read,
) {
Ok(s) => s,
Err(e) => {
audit_log!(audit, "Failed to begin search: {:?}", e);
return Err(e);
}
};
audit_log!(audit, "Begin event {:?}", srch);
// We have to use search_ext to guarantee acs was applied.
match qs_read.search_ext(&mut audit, &srch) {
Ok(mut entries) => {
let r = entries
.pop()
// From the entry, turn it into the value
.and_then(|e| {
e.get_ava_single("radius_secret")
.and_then(|v| v.get_radius_secret().map(|s| s.to_string()))
});
Ok(r)
}
Err(e) => Err(e),
}
});
self.log.do_send(audit);
res
}
}
impl Handler<InternalRadiusTokenReadMessage> for QueryServerReadV1 {
type Result = Result<RadiusAuthToken, OperationError>;
fn handle(
&mut self,
msg: InternalRadiusTokenReadMessage,
_: &mut Self::Context,
) -> Self::Result {
let mut audit = AuditScope::new("internal_radius_token_read_message");
let res = audit_segment!(&mut audit, || {
let idm_read = self.idms.proxy_read();
let target_uuid = match Uuid::parse_str(msg.uuid_or_name.as_str()) {
Ok(u) => u,
Err(_) => idm_read
.qs_read
.name_to_uuid(&mut audit, msg.uuid_or_name.as_str())
.map_err(|e| {
audit_log!(&mut audit, "Error resolving id to target");
e
})?,
};
// Make an event from the request
let rate = match RadiusAuthTokenEvent::from_parts(
&mut audit,
&idm_read.qs_read,
msg.uat,
target_uuid,
) {
Ok(s) => s,
Err(e) => {
audit_log!(audit, "Failed to begin search: {:?}", e);
return Err(e);
}
};
audit_log!(audit, "Begin event {:?}", rate);
idm_read.get_radiusauthtoken(&mut audit, &rate)
});
self.log.do_send(audit);
res
}
}

View file

@ -5,7 +5,7 @@ use crate::async_log::EventLog;
use crate::event::{
CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent,
};
use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent};
use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent, RegenerateRadiusSecretEvent};
use kanidm_proto::v1::OperationError;
use crate::idm::server::IdmServer;
@ -109,6 +109,36 @@ impl Message for InternalCredentialSetMessage {
type Result = Result<Option<String>, OperationError>;
}
pub struct InternalRegenerateRadiusMessage {
pub uat: Option<UserAuthToken>,
pub uuid_or_name: String,
}
impl InternalRegenerateRadiusMessage {
pub fn new(uat: Option<UserAuthToken>, uuid_or_name: String) -> Self {
InternalRegenerateRadiusMessage {
uat: uat,
uuid_or_name: uuid_or_name,
}
}
}
impl Message for InternalRegenerateRadiusMessage {
type Result = Result<String, OperationError>;
}
/// Indicate that we want to purge an attribute from the entry - this is generally
/// in response to a DELETE http method.
pub struct PurgeAttributeMessage {
pub uat: Option<UserAuthToken>,
pub uuid_or_name: String,
pub attr: String,
}
impl Message for PurgeAttributeMessage {
type Result = Result<(), OperationError>;
}
pub struct QueryServerWriteV1 {
log: actix::Addr<EventLog>,
qs: QueryServer,
@ -331,6 +361,95 @@ impl Handler<IdmAccountSetPasswordMessage> for QueryServerWriteV1 {
}
}
impl Handler<InternalRegenerateRadiusMessage> for QueryServerWriteV1 {
type Result = Result<String, OperationError>;
fn handle(
&mut self,
msg: InternalRegenerateRadiusMessage,
_: &mut Self::Context,
) -> Self::Result {
let mut audit = AuditScope::new("idm_account_regenerate_radius");
let res = audit_segment!(&mut audit, || {
let mut idms_prox_write = self.idms.proxy_write();
let target_uuid = match Uuid::parse_str(msg.uuid_or_name.as_str()) {
Ok(u) => u,
Err(_) => idms_prox_write
.qs_write
.name_to_uuid(&mut audit, msg.uuid_or_name.as_str())
.map_err(|e| {
audit_log!(&mut audit, "Error resolving id to target");
e
})?,
};
let rrse = RegenerateRadiusSecretEvent::from_parts(
&mut audit,
&idms_prox_write.qs_write,
msg.uat,
target_uuid,
)
.map_err(|e| {
audit_log!(
audit,
"Failed to begin idm_account_regenerate_radius: {:?}",
e
);
e
})?;
idms_prox_write
.regenerate_radius_secret(&mut audit, &rrse)
.and_then(|r| idms_prox_write.commit(&mut audit).map(|_| r))
});
self.log.do_send(audit);
res
}
}
impl Handler<PurgeAttributeMessage> for QueryServerWriteV1 {
type Result = Result<(), OperationError>;
fn handle(&mut self, msg: PurgeAttributeMessage, _: &mut Self::Context) -> Self::Result {
let mut audit = AuditScope::new("modify");
let res = audit_segment!(&mut audit, || {
let mut qs_write = self.qs.write();
let target_uuid = match Uuid::parse_str(msg.uuid_or_name.as_str()) {
Ok(u) => u,
Err(_) => qs_write
.name_to_uuid(&mut audit, msg.uuid_or_name.as_str())
.map_err(|e| {
audit_log!(&mut audit, "Error resolving id to target");
e
})?,
};
let mdf = match ModifyEvent::from_target_uuid_attr_purge(
&mut audit,
msg.uat,
target_uuid,
msg.attr,
&qs_write,
) {
Ok(m) => m,
Err(e) => {
audit_log!(audit, "Failed to begin modify: {:?}", e);
return Err(e);
}
};
audit_log!(audit, "Begin modify event {:?}", mdf);
qs_write
.modify(&mut audit, &mdf)
.and_then(|_| qs_write.commit(&mut audit).map(|_| ()))
});
self.log.do_send(audit);
res
}
}
// These below are internal only types.
impl Handler<PurgeTombstoneEvent> for QueryServerWriteV1 {

View file

@ -18,6 +18,12 @@ pub struct DbValueCredV1 {
pub d: DbCredV1,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct DbValueTaggedStringV1 {
pub t: String,
pub d: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum DbValueV1 {
U8(String),
@ -29,4 +35,6 @@ pub enum DbValueV1 {
RF(Uuid),
JF(String),
CR(DbValueCredV1),
RU(String),
SK(DbValueTaggedStringV1),
}

View file

@ -129,6 +129,8 @@ pub trait BackendTransaction {
}
}
FilterResolved::Or(l) => {
// Importantly if this has no inner elements, this returns
// an empty list.
let mut result = IDLBitRange::new();
let mut partial = false;
// For each filter in l
@ -1896,6 +1898,31 @@ mod tests {
panic!("");
}
}
// empty or
let f_e_or = unsafe { filter_resolved!(f_or!([])) };
let r = be.filter2idl(audit, f_e_or.to_inner(), 0).unwrap();
match r {
IDL::Indexed(idl) => {
assert!(idl == IDLBitRange::from_iter(vec![]));
}
_ => {
panic!("");
}
}
let f_e_and = unsafe { filter_resolved!(f_and!([])) };
let r = be.filter2idl(audit, f_e_and.to_inner(), 0).unwrap();
match r {
IDL::Indexed(idl) => {
assert!(idl == IDLBitRange::from_iter(vec![]));
}
_ => {
panic!("");
}
}
})
}

View file

@ -356,6 +356,7 @@ pub static JSON_IDM_SELF_ACP_READ_V1: &'static str = r#"{
"class",
"memberof",
"member",
"radius_secret",
"uuid"
]
}
@ -367,7 +368,7 @@ pub static JSON_IDM_SELF_ACP_WRITE_V1: &'static str = r#"{
"class": ["object", "access_control_profile", "access_control_modify"],
"name": ["idm_self_acp_write"],
"uuid": ["00000000-0000-0000-0000-ffffff000021"],
"description": ["Builtin IDM Control for self write - required for people to update their own identities in line with best practices."],
"description": ["Builtin IDM Control for self write - required for people to update their own identities and credentials in line with best practices."],
"acp_enable": ["true"],
"acp_receiver": [
"{\"And\": [\"Self\", {\"AndNot\": {\"Or\": [{\"Eq\": [\"class\", \"tombstone\"]}, {\"Eq\": [\"class\", \"recycled\"]}, {\"Eq\": [\"uuid\", \"00000000-0000-0000-0000-ffffffffffff\"]}]}}]}"
@ -376,10 +377,10 @@ pub static JSON_IDM_SELF_ACP_WRITE_V1: &'static str = r#"{
"\"Self\""
],
"acp_modify_removedattr": [
"name", "displayname", "legalname"
"name", "displayname", "legalname", "radius_secret", "primary_credential"
],
"acp_modify_presentattr": [
"name", "displayname", "legalname"
"name", "displayname", "legalname", "radius_secret", "primary_credential"
]
}
}"#;
@ -650,7 +651,6 @@ pub static JSON_IDM_ACP_PERSON_ACCOUNT_CREATE_V1: &'static str = r#"{
pub static _UUID_IDM_ACP_RADIUS_SERVERS_V1: &'static str = "00000000-0000-0000-0000-ffffff000014";
// The targetscope of this could change later to a "radius access" group or similar so we can add/remove
// users from having radius access easier.
// TODO #17: Add the radius credential type that we need to read here.
pub static JSON_IDM_ACP_RADIUS_SERVERS_V1: &'static str = r#"{
"attrs": {
"class": [
@ -669,7 +669,7 @@ pub static JSON_IDM_ACP_RADIUS_SERVERS_V1: &'static str = r#"{
"{\"And\": [{\"Pres\": \"class\"}, {\"AndNot\": {\"Or\": [{\"Eq\": [\"class\", \"tombstone\"]}, {\"Eq\": [\"class\", \"recycled\"]}]}}]}"
],
"acp_search_attr": [
"name", "uuid"
"name", "uuid", "radius_secret"
]
}
}"#;
@ -1178,7 +1178,7 @@ pub static JSON_SCHEMA_ATTR_SSH_PUBLICKEY: &'static str = r#"
"ssh_publickey"
],
"syntax": [
"UTF8STRING"
"SSHKEY"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000042"
@ -1253,6 +1253,35 @@ pub static JSON_SCHEMA_ATTR_LEGALNAME: &'static str = r#"{
]
}
}"#;
pub static UUID_SCHEMA_ATTR_RADIUS_SECRET: &'static str = "00000000-0000-0000-0000-ffff00000051";
pub static JSON_SCHEMA_ATTR_RADIUS_SECRET: &'static str = r#"{
"attrs": {
"class": [
"object",
"system",
"attributetype"
],
"description": [
"The accounts generated radius secret for device network authentication"
],
"index": [],
"unique": [
"false"
],
"multivalue": [
"false"
],
"attributename": [
"radius_secret"
],
"syntax": [
"RADIUS_UTF8STRING"
],
"uuid": [
"00000000-0000-0000-0000-ffff00000051"
]
}
}"#;
pub static UUID_SCHEMA_CLASS_PERSON: &'static str = "00000000-0000-0000-0000-ffff00000044";
pub static JSON_SCHEMA_CLASS_PERSON: &'static str = r#"
@ -1336,7 +1365,8 @@ pub static JSON_SCHEMA_CLASS_ACCOUNT: &'static str = r#"
],
"systemmay": [
"primary_credential",
"ssh_publickey"
"ssh_publickey",
"radius_secret"
],
"systemmust": [
"displayname",

View file

@ -15,11 +15,14 @@ use crate::config::Configuration;
// SearchResult
use crate::actors::v1_read::QueryServerReadV1;
use crate::actors::v1_read::{AuthMessage, InternalSearchMessage, SearchMessage, WhoamiMessage};
use crate::actors::v1_read::{
AuthMessage, InternalRadiusReadMessage, InternalRadiusTokenReadMessage, InternalSearchMessage,
SearchMessage, WhoamiMessage,
};
use crate::actors::v1_write::QueryServerWriteV1;
use crate::actors::v1_write::{
CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage, InternalCredentialSetMessage,
ModifyMessage,
InternalRegenerateRadiusMessage, ModifyMessage, PurgeAttributeMessage,
};
use crate::async_log;
use crate::audit::AuditScope;
@ -187,12 +190,17 @@ fn json_rest_event_get(
req: HttpRequest<AppState>,
state: State<AppState>,
filter: Filter<FilterInvalid>,
attrs: Option<Vec<String>>,
) -> impl Future<Item = HttpResponse, Error = Error> {
let uat = get_current_user(&req);
// TODO: I think we'll need to change this to take an internal filter
// type that we send to the qs.
let obj = InternalSearchMessage::new(uat, filter);
let obj = InternalSearchMessage {
uat: uat,
filter: filter,
attrs: attrs,
};
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)),
@ -207,12 +215,17 @@ fn json_rest_event_get_id(
req: HttpRequest<AppState>,
state: State<AppState>,
filter: Filter<FilterInvalid>,
attrs: Option<Vec<String>>,
) -> impl Future<Item = HttpResponse, Error = Error> {
let uat = get_current_user(&req);
let filter = Filter::join_parts_and(filter, filter_all!(f_id(path.as_str())));
let obj = InternalSearchMessage::new(uat, filter);
let obj = InternalSearchMessage {
uat: uat,
filter: filter,
attrs: attrs,
};
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
Ok(mut event_result) => {
@ -225,6 +238,71 @@ fn json_rest_event_get_id(
Box::new(res)
}
fn json_rest_event_get_id_attr(
path: Path<String>,
req: HttpRequest<AppState>,
state: State<AppState>,
filter: Filter<FilterInvalid>,
attr: String,
) -> impl Future<Item = HttpResponse, Error = Error> {
let uat = get_current_user(&req);
let filter = Filter::join_parts_and(filter, filter_all!(f_id(path.as_str())));
let obj = InternalSearchMessage {
uat: uat,
filter: filter,
attrs: Some(vec![attr.clone()]),
};
let res = state
.qe_r
.send(obj)
.from_err()
.and_then(move |res| match res {
Ok(mut event_result) => {
// TODO: Check this only has len 1, even though that satte should be impossible.
// Only get one result
let r = event_result.pop().and_then(|mut e| {
// Only get the attribute as requested.
e.attrs.remove(&attr)
});
debug!("final json result {:?}", r);
// Only send back the first result, or None
Ok(HttpResponse::Ok().json(r))
}
Err(e) => Ok(operation_error_to_response(e)),
});
Box::new(res)
}
fn json_rest_event_delete_id_attr(
path: Path<String>,
req: HttpRequest<AppState>,
state: State<AppState>,
attr: String,
) -> impl Future<Item = HttpResponse, Error = Error> {
let uat = get_current_user(&req);
let id = path.into_inner();
let obj = PurgeAttributeMessage {
uat: uat,
uuid_or_name: id,
attr: attr,
};
let res = state.qe_w.send(obj).from_err().and_then(|res| match res {
Ok(event_result) => {
// Only send back the first result, or None
Ok(HttpResponse::Ok().json(event_result))
}
Err(e) => Ok(operation_error_to_response(e)),
});
Box::new(res)
}
fn json_rest_event_credential_put(
id: String,
cred_id: Option<String>,
@ -299,14 +377,14 @@ fn schema_get(
f_eq("class", PartialValue::new_class("attributetype")),
f_eq("class", PartialValue::new_class("classtype"))
]));
json_rest_event_get(req, state, filter)
json_rest_event_get(req, state, filter, None)
}
fn schema_attributetype_get(
(req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
let filter = filter_all!(f_eq("class", PartialValue::new_class("attributetype")));
json_rest_event_get(req, state, filter)
json_rest_event_get(req, state, filter, None)
}
fn schema_attributetype_get_id(
@ -320,7 +398,11 @@ fn schema_attributetype_get_id(
f_eq("attributename", PartialValue::new_iutf8s(path.as_str()))
]));
let obj = InternalSearchMessage::new(uat, filter);
let obj = InternalSearchMessage {
uat: uat,
filter: filter,
attrs: None,
};
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
Ok(mut event_result) => {
@ -337,7 +419,7 @@ fn schema_classtype_get(
(req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
let filter = filter_all!(f_eq("class", PartialValue::new_class("classtype")));
json_rest_event_get(req, state, filter)
json_rest_event_get(req, state, filter, None)
}
fn schema_classtype_get_id(
@ -351,7 +433,11 @@ fn schema_classtype_get_id(
f_eq("classname", PartialValue::new_iutf8s(path.as_str()))
]));
let obj = InternalSearchMessage::new(uat, filter);
let obj = InternalSearchMessage {
uat: uat,
filter: filter,
attrs: None,
};
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
Ok(mut event_result) => {
@ -368,14 +454,14 @@ fn account_get(
(req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_get(req, state, filter)
json_rest_event_get(req, state, filter, None)
}
fn account_get_id(
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_get_id(path, req, state, filter)
json_rest_event_get_id(path, req, state, filter, None)
}
fn account_put_id_credential_primary(
@ -385,18 +471,89 @@ fn account_put_id_credential_primary(
json_rest_event_credential_put(id, None, req, state)
}
// Get and return a single str
fn account_get_id_radius(
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
let uat = get_current_user(&req);
let id = path.into_inner();
let obj = InternalRadiusReadMessage {
uat: uat,
uuid_or_name: id,
};
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
Ok(event_result) => {
// Only send back the first result, or None
Ok(HttpResponse::Ok().json(event_result))
}
Err(e) => Ok(operation_error_to_response(e)),
});
Box::new(res)
}
fn account_post_id_radius_regenerate(
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
// Need to to send the regen msg
let uat = get_current_user(&req);
let id = path.into_inner();
let obj = InternalRegenerateRadiusMessage::new(uat, id);
let res = state.qe_w.send(obj).from_err().and_then(|res| match res {
Ok(event_result) => {
// Only send back the first result, or None
Ok(HttpResponse::Ok().json(event_result))
}
Err(e) => Ok(operation_error_to_response(e)),
});
Box::new(res)
}
fn account_delete_id_radius(
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
json_rest_event_delete_id_attr(path, req, state, "radius_secret".to_string())
}
fn account_get_id_radius_token(
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
let uat = get_current_user(&req);
let id = path.into_inner();
let obj = InternalRadiusTokenReadMessage {
uat: uat,
uuid_or_name: id,
};
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
Ok(event_result) => {
// Only send back the first result, or None
Ok(HttpResponse::Ok().json(event_result))
}
Err(e) => Ok(operation_error_to_response(e)),
});
Box::new(res)
}
fn group_get(
(req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
json_rest_event_get(req, state, filter)
json_rest_event_get(req, state, filter, None)
}
fn group_id_get(
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
json_rest_event_get_id(path, req, state, filter)
json_rest_event_get_id(path, req, state, filter, None)
}
fn do_nothing((_req, _state): (HttpRequest<AppState>, State<AppState>)) -> String {
@ -972,11 +1129,19 @@ pub fn create_server_core(config: Configuration) {
// Can we self lock?
})
.resource("/v1/self/_radius", |r| {
// Get our radius secret for manual configuration
r.method(http::Method::GET).with(do_nothing)
// more to be added
})
.resource("/v1/self/_radius", |r| {
// delete our radius secret
r.method(http::Method::DELETE).with(do_nothing)
})
.resource("/v1/self/_radius", |r| {
// regenerate our radius secret
r.method(http::Method::POST).with(do_nothing)
})
.resource("/v1/self/_radius/_config", |r| {
// Create new secret_otp?
// Create new secret_otp for client configuration
r.method(http::Method::POST).with(do_nothing)
})
.resource("/v1/self/_radius/_config/{secret_otp}", |r| {
@ -1019,11 +1184,17 @@ pub fn create_server_core(config: Configuration) {
// add post, delete
})
.resource("/v1/account/{id}/_radius", |r| {
r.method(http::Method::GET).with(do_nothing)
// more to be added
r.method(http::Method::GET)
.with_async(account_get_id_radius);
r.method(http::Method::POST)
.with_async(account_post_id_radius_regenerate);
r.method(http::Method::DELETE)
.with_async(account_delete_id_radius);
})
// This is how the radius server views a json blob about the ID and radius creds.
.resource("/v1/account/{id}/_radius/_token", |r| {
r.method(http::Method::GET).with(do_nothing)
r.method(http::Method::GET)
.with_async(account_get_id_radius_token)
})
// Groups
.resource("/v1/group", |r| {

View file

@ -162,7 +162,9 @@ pub struct EntryInvalid;
// pub struct EntryNormalised;
#[derive(Clone, Copy, Debug)]
pub struct EntryReduced;
pub struct EntryReduced {
uuid: Uuid,
}
#[derive(Debug)]
pub struct Entry<VALID, STATE> {
@ -980,10 +982,11 @@ impl Entry<EntryValid, EntryCommitted> {
})
}
#[cfg(test)]
pub fn to_reduced(self) -> Entry<EntryReduced, EntryCommitted> {
pub unsafe fn to_reduced(self) -> Entry<EntryReduced, EntryCommitted> {
Entry {
valid: EntryReduced,
valid: EntryReduced {
uuid: self.valid.uuid,
},
state: self.state,
attrs: self.attrs,
}
@ -996,7 +999,7 @@ impl Entry<EntryValid, EntryCommitted> {
// Remove all attrs from our tree that are NOT in the allowed set.
let Entry {
valid: _s_valid,
valid: s_valid,
state: s_state,
attrs: s_attrs,
} = self;
@ -1013,135 +1016,11 @@ impl Entry<EntryValid, EntryCommitted> {
.collect();
Entry {
valid: EntryReduced,
valid: EntryReduced { uuid: s_valid.uuid },
state: s_state,
attrs: f_attrs,
}
}
// These are special types to allow returning typed values from
// an entry, if we "know" what we expect to receive.
/// This returns an array of IndexTypes, when the type is an Optional
/// multivalue in schema - IE this will *not* fail if the attribute is
/// empty, yielding and empty array instead.
///
/// However, the converstion to IndexType is fallaible, so in case of a failure
/// to convert, an Err is returned.
pub(crate) fn get_ava_opt_index(&self, attr: &str) -> Result<Vec<&IndexType>, ()> {
match self.attrs.get(attr) {
Some(av) => {
let r: Result<Vec<_>, _> = av.iter().map(|v| v.to_indextype().ok_or(())).collect();
r
}
None => Ok(Vec::new()),
}
}
/// Get a bool from an ava
pub fn get_ava_single_bool(&self, attr: &str) -> Option<bool> {
match self.get_ava_single(attr) {
Some(a) => a.to_bool(),
None => None,
}
}
pub fn get_ava_single_syntax(&self, attr: &str) -> Option<&SyntaxType> {
match self.get_ava_single(attr) {
Some(a) => a.to_syntaxtype(),
None => None,
}
}
pub fn get_ava_single_credential(&self, attr: &str) -> Option<&Credential> {
match self.get_ava_single(attr) {
Some(a) => a.to_credential(),
None => None,
}
}
pub fn get_ava_reference_uuid(&self, attr: &str) -> Option<Vec<&Uuid>> {
// If any value is NOT a reference, return none!
match self.attrs.get(attr) {
Some(av) => {
let v: Option<Vec<&Uuid>> = av.iter().map(|e| e.to_ref_uuid()).collect();
v
}
None => None,
}
}
/*
/// This interface will get &str (if possible).
pub(crate) fn get_ava_opt_str(&self, attr: &str) -> Option<Vec<&str>> {
match self.attrs.get(attr) {
Some(a) => {
let r: Vec<_> = a.iter().filter_map(|v| v.to_str()).collect();
if r.len() == 0 {
None
} else {
Some(r)
}
}
None => Some(Vec::new()),
}
}
*/
pub(crate) fn get_ava_opt_string(&self, attr: &str) -> Option<Vec<String>> {
match self.attrs.get(attr) {
Some(a) => {
let r: Vec<String> = a
.iter()
.filter_map(|v| v.as_string().map(|s| s.clone()))
.collect();
if r.len() == 0 {
// Corrupt?
None
} else {
Some(r)
}
}
None => Some(Vec::new()),
}
}
pub(crate) fn get_ava_string(&self, attr: &str) -> Option<Vec<String>> {
match self.attrs.get(attr) {
Some(a) => {
let r: Vec<String> = a
.iter()
.filter_map(|v| v.as_string().map(|s| s.clone()))
.collect();
if r.len() == 0 {
// Corrupt?
None
} else {
Some(r)
}
}
None => None,
}
}
pub fn get_ava_single_str(&self, attr: &str) -> Option<&str> {
self.get_ava_single(attr).and_then(|v| v.to_str())
}
pub fn get_ava_single_string(&self, attr: &str) -> Option<String> {
self.get_ava_single(attr)
.and_then(|v: &Value| v.as_string())
.and_then(|s: &String| Some((*s).clone()))
}
pub fn get_ava_single_protofilter(&self, attr: &str) -> Option<ProtoFilter> {
self.get_ava_single(attr)
.and_then(|v: &Value| {
debug!("get_ava_single_protofilter -> {:?}", v);
v.as_json_filter()
})
.and_then(|f: &ProtoFilter| Some((*f).clone()))
}
}
impl<STATE> Entry<EntryValid, STATE> {
@ -1244,6 +1123,10 @@ impl<STATE> Entry<EntryValid, STATE> {
}
impl Entry<EntryReduced, EntryCommitted> {
pub fn get_uuid(&self) -> &Uuid {
&self.valid.uuid
}
pub fn into_pe(
&self,
audit: &mut AuditScope,
@ -1316,6 +1199,150 @@ impl<VALID, STATE> Entry<VALID, STATE> {
r
}
pub fn get_ava_reference_uuid(&self, attr: &str) -> Option<Vec<&Uuid>> {
// If any value is NOT a reference, return none!
match self.attrs.get(attr) {
Some(av) => {
let v: Option<Vec<&Uuid>> = av.iter().map(|e| e.to_ref_uuid()).collect();
v
}
None => None,
}
}
// These are special types to allow returning typed values from
// an entry, if we "know" what we expect to receive.
/// This returns an array of IndexTypes, when the type is an Optional
/// multivalue in schema - IE this will *not* fail if the attribute is
/// empty, yielding and empty array instead.
///
/// However, the converstion to IndexType is fallaible, so in case of a failure
/// to convert, an Err is returned.
pub(crate) fn get_ava_opt_index(&self, attr: &str) -> Result<Vec<&IndexType>, ()> {
match self.attrs.get(attr) {
Some(av) => {
let r: Result<Vec<_>, _> = av.iter().map(|v| v.to_indextype().ok_or(())).collect();
r
}
None => Ok(Vec::new()),
}
}
/// Get a bool from an ava
pub fn get_ava_single_bool(&self, attr: &str) -> Option<bool> {
match self.get_ava_single(attr) {
Some(a) => a.to_bool(),
None => None,
}
}
pub fn get_ava_single_syntax(&self, attr: &str) -> Option<&SyntaxType> {
match self.get_ava_single(attr) {
Some(a) => a.to_syntaxtype(),
None => None,
}
}
pub fn get_ava_single_credential(&self, attr: &str) -> Option<&Credential> {
self.get_ava_single(attr).and_then(|a| a.to_credential())
}
pub fn get_ava_single_radiuscred(&self, attr: &str) -> Option<&str> {
self.get_ava_single(attr)
.and_then(|a| a.get_radius_secret())
}
/*
/// This interface will get &str (if possible).
pub(crate) fn get_ava_opt_str(&self, attr: &str) -> Option<Vec<&str>> {
match self.attrs.get(attr) {
Some(a) => {
let r: Vec<_> = a.iter().filter_map(|v| v.to_str()).collect();
if r.len() == 0 {
None
} else {
Some(r)
}
}
None => Some(Vec::new()),
}
}
*/
pub(crate) fn get_ava_opt_string(&self, attr: &str) -> Option<Vec<String>> {
match self.attrs.get(attr) {
Some(a) => {
let r: Vec<String> = a
.iter()
.filter_map(|v| v.as_string().map(|s| s.clone()))
.collect();
if r.len() == 0 {
// Corrupt?
None
} else {
Some(r)
}
}
None => Some(Vec::new()),
}
}
pub(crate) fn get_ava_string(&self, attr: &str) -> Option<Vec<String>> {
match self.attrs.get(attr) {
Some(a) => {
let r: Vec<String> = a
.iter()
.filter_map(|v| v.as_string().map(|s| s.clone()))
.collect();
if r.len() == 0 {
// Corrupt?
None
} else {
Some(r)
}
}
None => None,
}
}
pub(crate) fn get_ava_set_string(&self, attr: &str) -> Option<BTreeSet<String>> {
match self.attrs.get(attr) {
Some(a) => {
let r: BTreeSet<String> = a
.iter()
.filter_map(|v| v.as_string().map(|s| s.clone()))
.collect();
if r.len() == 0 {
// Corrupt?
None
} else {
Some(r)
}
}
None => None,
}
}
pub fn get_ava_single_str(&self, attr: &str) -> Option<&str> {
self.get_ava_single(attr).and_then(|v| v.to_str())
}
pub fn get_ava_single_string(&self, attr: &str) -> Option<String> {
self.get_ava_single(attr)
.and_then(|v: &Value| v.as_string())
.and_then(|s: &String| Some((*s).clone()))
}
pub fn get_ava_single_protofilter(&self, attr: &str) -> Option<ProtoFilter> {
self.get_ava_single(attr)
.and_then(|v: &Value| {
debug!("get_ava_single_protofilter -> {:?}", v);
v.as_json_filter()
})
.and_then(|f: &ProtoFilter| Some((*f).clone()))
}
pub fn attribute_pres(&self, attr: &str) -> bool {
// Note, we don't normalise attr name, but I think that's not
// something we should over-optimise on.

View file

@ -1,6 +1,8 @@
use crate::audit::AuditScope;
use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, EntryValid};
use crate::filter::{Filter, FilterValid};
use crate::schema::SchemaTransaction;
use crate::value::PartialValue;
use kanidm_proto::v1::Entry as ProtoEntry;
use kanidm_proto::v1::{
AuthCredential, AuthResponse, AuthState, AuthStep, SearchResponse, UserAuthToken,
@ -25,6 +27,7 @@ use crate::filter::FilterInvalid;
use crate::modify::ModifyInvalid;
use actix::prelude::*;
use std::collections::BTreeSet;
use uuid::Uuid;
#[derive(Debug)]
@ -217,7 +220,7 @@ pub struct SearchEvent {
pub filter: Filter<FilterValid>,
// This is the original filter, for the purpose of ACI checking.
pub filter_orig: Filter<FilterValid>,
// TODO #83: Add list of attributes to request
pub attrs: Option<BTreeSet<String>>,
}
impl SearchEvent {
@ -239,6 +242,9 @@ impl SearchEvent {
filter_orig: f
.validate(qs.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?,
// We can't get this from the SearchMessage because it's annoying with the
// current macro design.
attrs: None,
}),
Err(e) => Err(e),
}
@ -249,6 +255,21 @@ impl SearchEvent {
msg: InternalSearchMessage,
qs: &QueryServerReadTransaction,
) -> Result<Self, OperationError> {
let r_attrs: Option<BTreeSet<String>> = msg.attrs.map(|vs| {
vs.into_iter()
.filter_map(|a| qs.get_schema().normalise_attr_if_exists(a.as_str()))
.collect()
});
match &r_attrs {
Some(s) => {
if s.len() == 0 {
return Err(OperationError::EmptyRequest);
}
}
_ => {}
}
Ok(SearchEvent {
event: Event::from_ro_uat(audit, qs, msg.uat)?,
// We do need to do this twice to account for the ignore_hidden
@ -263,6 +284,7 @@ impl SearchEvent {
.filter
.validate(qs.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?,
attrs: r_attrs,
})
}
@ -279,6 +301,26 @@ impl SearchEvent {
filter_orig: filter_all!(f_self())
.validate(qs.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?,
// TODO: Should we limit this?
attrs: None,
})
}
pub fn from_target_uuid_request(
audit: &mut AuditScope,
uat: Option<UserAuthToken>,
target_uuid: Uuid,
qs: &QueryServerReadTransaction,
) -> Result<Self, OperationError> {
Ok(SearchEvent {
event: Event::from_ro_uat(audit, qs, uat)?,
filter: filter!(f_eq("uuid", PartialValue::new_uuid(target_uuid.clone())))
.validate(qs.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?,
filter_orig: filter_all!(f_eq("uuid", PartialValue::new_uuid(target_uuid)))
.validate(qs.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?,
attrs: None,
})
}
@ -289,6 +331,7 @@ impl SearchEvent {
event: Event::from_impersonate_entry_ser(e),
filter: filter.clone().to_valid(),
filter_orig: filter.to_valid(),
attrs: None,
}
}
@ -301,6 +344,7 @@ impl SearchEvent {
event: Event::from_impersonate_entry(e),
filter: filter.clone().to_valid(),
filter_orig: filter.to_valid(),
attrs: None,
}
}
@ -313,6 +357,7 @@ impl SearchEvent {
event: Event::from_impersonate(event),
filter: filter,
filter_orig: filter_orig,
attrs: None,
}
}
@ -351,6 +396,7 @@ impl SearchEvent {
event: Event::from_impersonate_entry(e),
filter: filter.clone().to_recycled().to_valid(),
filter_orig: filter.to_valid(),
attrs: None,
}
}
@ -364,6 +410,7 @@ impl SearchEvent {
event: Event::from_impersonate_entry(e),
filter: filter.clone().to_ignore_hidden().to_valid(),
filter_orig: filter.to_valid(),
attrs: None,
}
}
@ -373,6 +420,7 @@ impl SearchEvent {
event: Event::from_internal(),
filter: filter.clone().to_valid(),
filter_orig: filter.to_valid(),
attrs: None,
}
}
@ -381,6 +429,7 @@ impl SearchEvent {
event: Event::from_internal(),
filter: filter.clone(),
filter_orig: filter,
attrs: None,
}
}
}
@ -580,6 +629,31 @@ impl ModifyEvent {
}
}
pub fn from_target_uuid_attr_purge(
audit: &mut AuditScope,
uat: Option<UserAuthToken>,
target_uuid: Uuid,
attr: String,
qs: &QueryServerWriteTransaction,
) -> Result<Self, OperationError> {
let ml = ModifyList::new_purge(attr.as_str());
let f = filter_all!(f_eq("uuid", PartialValue::new_uuid(target_uuid)));
Ok(ModifyEvent {
event: Event::from_rw_uat(audit, qs, uat)?,
filter: f
.clone()
.to_ignore_hidden()
.validate(qs.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?,
filter_orig: f
.validate(qs.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?,
modlist: ml
.validate(qs.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?,
})
}
pub fn new_internal(filter: Filter<FilterValid>, modlist: ModifyList<ModifyValid>) -> Self {
ModifyEvent {
event: Event::from_internal(),

View file

@ -3,11 +3,13 @@ use kanidm_proto::v1::OperationError;
use kanidm_proto::v1::UserAuthToken;
use crate::audit::AuditScope;
use crate::constants::UUID_ANONYMOUS;
use crate::credential::Credential;
use crate::idm::claim::Claim;
use crate::idm::group::Group;
use crate::modify::{ModifyInvalid, ModifyList};
use crate::server::{QueryServerReadTransaction, QueryServerWriteTransaction};
use crate::value::{PartialValue, Value};
use uuid::Uuid;
@ -16,6 +18,46 @@ lazy_static! {
static ref PVCLASS_ACCOUNT: PartialValue = PartialValue::new_class("account");
}
macro_rules! try_from_entry {
($value:expr, $groups:expr) => {{
// Check the classes
if !$value.attribute_value_pres("class", &PVCLASS_ACCOUNT) {
return Err(OperationError::InvalidAccountState(
"Missing class: account".to_string(),
));
}
// Now extract our needed attributes
let name =
$value
.get_ava_single_string("name")
.ok_or(OperationError::InvalidAccountState(
"Missing attribute: name".to_string(),
))?;
let displayname = $value.get_ava_single_string("displayname").ok_or(
OperationError::InvalidAccountState("Missing attribute: displayname".to_string()),
)?;
let primary = $value
.get_ava_single_credential("primary_credential")
.map(|v| v.clone());
// Resolved by the caller
let groups = $groups;
let uuid = $value.get_uuid().clone();
Ok(Account {
uuid: uuid,
name: name,
displayname: displayname,
groups: groups,
primary: primary,
})
}};
}
#[derive(Debug, Clone)]
pub(crate) struct Account {
// Later these could be &str if we cache entry here too ...
@ -35,45 +77,29 @@ pub(crate) struct Account {
}
impl Account {
// TODO #71: We need a second try_from that doesn't do group resolve for test cases I think.
pub(crate) fn try_from_entry(
pub(crate) fn try_from_entry_ro(
au: &mut AuditScope,
value: Entry<EntryValid, EntryCommitted>,
qs: &QueryServerReadTransaction,
) -> Result<Self, OperationError> {
// Check the classes
if !value.attribute_value_pres("class", &PVCLASS_ACCOUNT) {
return Err(OperationError::InvalidAccountState(
"Missing class: account".to_string(),
));
let groups = Group::try_from_account_entry_ro(au, &value, qs)?;
try_from_entry!(value, groups)
}
// Now extract our needed attributes
let name =
value
.get_ava_single_string("name")
.ok_or(OperationError::InvalidAccountState(
"Missing attribute: name".to_string(),
))?;
pub(crate) fn try_from_entry_rw(
au: &mut AuditScope,
value: Entry<EntryValid, EntryCommitted>,
qs: &QueryServerWriteTransaction,
) -> Result<Self, OperationError> {
let groups = Group::try_from_account_entry_rw(au, &value, qs)?;
try_from_entry!(value, groups)
}
let displayname = value.get_ava_single_string("displayname").ok_or(
OperationError::InvalidAccountState("Missing attribute: displayname".to_string()),
)?;
let primary = value
.get_ava_single_credential("primary_credential")
.map(|v| v.clone());
// TODO #71: Resolve groups!!!!
let groups = Vec::new();
let uuid = value.get_uuid().clone();
Ok(Account {
uuid: uuid,
name: name,
displayname: displayname,
groups: groups,
primary: primary,
})
#[cfg(test)]
pub(crate) fn try_from_entry_no_groups(
value: Entry<EntryValid, EntryCommitted>,
) -> Result<Self, OperationError> {
try_from_entry!(value, vec![])
}
// Could this actually take a claims list and application instead?
@ -127,6 +153,14 @@ impl Account {
} // no appid
}
}
pub(crate) fn regenerate_radius_secret_mod(
&self,
cleartext: &str,
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
let vcred = Value::new_radius_str(cleartext);
Ok(ModifyList::new_purge_and_set("radius_secret", vcred))
}
}
// Need to also add a "to UserAuthToken" ...
@ -145,7 +179,7 @@ mod tests {
unsafe { Entry::unsafe_from_entry_str(JSON_ANONYMOUS_V1).to_valid_new() };
let anon_e = unsafe { anon_e.to_valid_committed() };
let anon_account = Account::try_from_entry(anon_e).expect("Must not fail");
let anon_account = Account::try_from_entry_no_groups(anon_e).expect("Must not fail");
println!("{:?}", anon_account);
// I think that's it? we may want to check anonymous mech ...
}

View file

@ -1,7 +1,7 @@
use crate::actors::v1_write::IdmAccountSetPasswordMessage;
use crate::audit::AuditScope;
use crate::event::Event;
use crate::server::QueryServerWriteTransaction;
use crate::server::{QueryServerReadTransaction, QueryServerWriteTransaction};
use uuid::Uuid;
@ -84,3 +84,67 @@ impl GeneratePasswordEvent {
})
}
}
#[derive(Debug)]
pub struct RegenerateRadiusSecretEvent {
pub event: Event,
pub target: Uuid,
}
impl RegenerateRadiusSecretEvent {
pub fn from_parts(
audit: &mut AuditScope,
qs: &QueryServerWriteTransaction,
uat: Option<UserAuthToken>,
target: Uuid,
) -> Result<Self, OperationError> {
let e = Event::from_rw_uat(audit, qs, uat)?;
Ok(RegenerateRadiusSecretEvent {
event: e,
target: target,
})
}
#[cfg(test)]
pub fn new_internal(target: Uuid) -> Self {
let e = Event::from_internal();
RegenerateRadiusSecretEvent {
event: e,
target: target,
}
}
}
#[derive(Debug)]
pub struct RadiusAuthTokenEvent {
pub event: Event,
pub target: Uuid,
}
impl RadiusAuthTokenEvent {
pub fn from_parts(
audit: &mut AuditScope,
qs: &QueryServerReadTransaction,
uat: Option<UserAuthToken>,
target: Uuid,
) -> Result<Self, OperationError> {
let e = Event::from_ro_uat(audit, qs, uat)?;
Ok(RadiusAuthTokenEvent {
event: e,
target: target,
})
}
#[cfg(test)]
pub fn new_internal(target: Uuid) -> Self {
let e = Event::from_internal();
RadiusAuthTokenEvent {
event: e,
target: target,
}
}
}

View file

@ -1,19 +1,111 @@
use crate::audit::AuditScope;
use crate::entry::{Entry, EntryCommitted, EntryReduced, EntryValid};
use crate::server::{
QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction,
};
use crate::value::PartialValue;
use kanidm_proto::v1::Group as ProtoGroup;
use kanidm_proto::v1::OperationError;
use uuid::Uuid;
lazy_static! {
static ref PVCLASS_GROUP: PartialValue = PartialValue::new_class("group");
}
#[derive(Debug, Clone)]
pub struct Group {
// name
// uuid
name: String,
uuid: Uuid,
// We'll probably add policy and claims later to this
}
macro_rules! try_from_account_e {
($au:expr, $value:expr, $qs:expr) => {{
let groups: Vec<Group> = match $value.get_ava_reference_uuid("memberof") {
Some(l) => {
// given a list of uuid, make a filter: even if this is empty, the be will
// just give and empty result set.
let f = filter!(f_or(
l.into_iter()
.map(|u| f_eq("uuid", PartialValue::new_uuidr(u)))
.collect()
));
let ges: Vec<_> = $qs.internal_search($au, f).map_err(|e| {
// log
e
})?;
// Now convert the group entries to groups.
let groups: Result<Vec<_>, _> =
ges.into_iter().map(|e| Group::try_from_entry(e)).collect();
groups.map_err(|e| {
// log
e
})?
}
None => {
// No memberof, no groups!
vec![]
}
};
Ok(groups)
}};
}
impl Group {
/*
pub fn new() -> Self {
Group {}
pub fn try_from_account_entry_red_ro(
au: &mut AuditScope,
value: &Entry<EntryReduced, EntryCommitted>,
qs: &QueryServerReadTransaction,
) -> Result<Vec<Self>, OperationError> {
try_from_account_e!(au, value, qs)
}
pub fn try_from_account_entry_ro(
au: &mut AuditScope,
value: &Entry<EntryValid, EntryCommitted>,
qs: &QueryServerReadTransaction,
) -> Result<Vec<Self>, OperationError> {
try_from_account_e!(au, value, qs)
}
pub fn try_from_account_entry_rw(
au: &mut AuditScope,
value: &Entry<EntryValid, EntryCommitted>,
qs: &QueryServerWriteTransaction,
) -> Result<Vec<Self>, OperationError> {
try_from_account_e!(au, value, qs)
}
pub fn try_from_entry(
value: Entry<EntryValid, EntryCommitted>,
) -> Result<Self, OperationError> {
if !value.attribute_value_pres("class", &PVCLASS_GROUP) {
return Err(OperationError::InvalidAccountState(
"Missing class: group".to_string(),
));
}
// Now extract our needed attributes
let name =
value
.get_ava_single_string("name")
.ok_or(OperationError::InvalidAccountState(
"Missing attribute: name".to_string(),
))?;
let uuid = value.get_uuid().clone();
Ok(Group {
name: name,
uuid: uuid,
})
}
*/
pub fn into_proto(&self) -> ProtoGroup {
unimplemented!();
ProtoGroup {
name: self.name.clone(),
uuid: self.uuid.to_hyphenated_ref().to_string(),
}
}
}

View file

@ -8,7 +8,7 @@ macro_rules! entry_str_to_account {
unsafe { Entry::unsafe_from_entry_str($entry_str).to_valid_new() };
let e = unsafe { e.to_valid_committed() };
Account::try_from_entry(e).expect("Account conversion failure")
Account::try_from_entry_no_groups(e).expect("Account conversion failure")
}};
}

View file

@ -6,5 +6,6 @@ pub(crate) mod authsession;
pub(crate) mod claim;
pub(crate) mod event;
pub(crate) mod group;
pub(crate) mod radius;
pub(crate) mod server;
// mod identity;

View file

@ -0,0 +1,78 @@
use crate::idm::group::Group;
use uuid::Uuid;
use crate::audit::AuditScope;
use crate::entry::{Entry, EntryCommitted, EntryReduced};
use crate::server::QueryServerReadTransaction;
use crate::value::PartialValue;
use kanidm_proto::v1::OperationError;
use kanidm_proto::v1::RadiusAuthToken;
lazy_static! {
static ref PVCLASS_ACCOUNT: PartialValue = PartialValue::new_class("account");
}
#[derive(Debug, Clone)]
pub(crate) struct RadiusAccount {
pub name: String,
pub displayname: String,
pub uuid: Uuid,
pub groups: Vec<Group>,
pub radius_secret: String,
}
impl RadiusAccount {
pub(crate) fn try_from_entry_reduced(
au: &mut AuditScope,
value: Entry<EntryReduced, EntryCommitted>,
qs: &QueryServerReadTransaction,
) -> Result<Self, OperationError> {
if !value.attribute_value_pres("class", &PVCLASS_ACCOUNT) {
return Err(OperationError::InvalidAccountState(
"Missing class: account".to_string(),
));
}
let radius_secret = value
.get_ava_single_radiuscred("radius_secret")
.ok_or(OperationError::InvalidAccountState(
"Missing attribute: radius_secret".to_string(),
))?
.to_string();
let name =
value
.get_ava_single_string("name")
.ok_or(OperationError::InvalidAccountState(
"Missing attribute: name".to_string(),
))?;
let uuid = value.get_uuid().clone();
let displayname = value.get_ava_single_string("displayname").ok_or(
OperationError::InvalidAccountState("Missing attribute: displayname".to_string()),
)?;
let groups = Group::try_from_account_entry_red_ro(au, &value, qs)?;
Ok(RadiusAccount {
name: name,
uuid: uuid,
displayname: displayname,
groups: groups,
radius_secret: radius_secret,
})
}
pub(crate) fn to_radiusauthtoken(&self) -> Result<RadiusAuthToken, OperationError> {
// If we don't have access/permission, then just error instead.
// This includes if we don't have the secret.
Ok(RadiusAuthToken {
name: self.name.clone(),
displayname: self.displayname.clone(),
uuid: self.uuid.to_hyphenated_ref().to_string(),
secret: self.radius_secret.clone(),
groups: self.groups.iter().map(|g| g.into_proto()).collect(),
})
}
}

View file

@ -3,13 +3,18 @@ use crate::constants::AUTH_SESSION_TIMEOUT;
use crate::event::{AuthEvent, AuthEventStep, AuthResult};
use crate::idm::account::Account;
use crate::idm::authsession::AuthSession;
use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent};
use crate::idm::event::{
GeneratePasswordEvent, PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
};
use crate::idm::radius::RadiusAccount;
use crate::server::QueryServerReadTransaction;
use crate::server::{QueryServer, QueryServerTransaction, QueryServerWriteTransaction};
use crate::utils::{password_from_random, uuid_from_duration, SID};
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, SID};
use crate::value::PartialValue;
use kanidm_proto::v1::AuthState;
use kanidm_proto::v1::OperationError;
use kanidm_proto::v1::RadiusAuthToken;
use concread::cowcell::{CowCell, CowCellWriteTxn};
use std::collections::BTreeMap;
@ -21,9 +26,6 @@ pub struct IdmServer {
// means that limits to sessions can be easily applied and checked to
// variaous accounts, and we have a good idea of how to structure the
// in memory caches related to locking.
//
// TODO #60: This needs a mark-and-sweep gc to be added.
// use split_off()
sessions: CowCell<BTreeMap<Uuid, AuthSession>>,
// Need a reference to the query server.
qs: QueryServer,
@ -40,13 +42,11 @@ pub struct IdmServerWriteTransaction<'a> {
sid: &'a SID,
}
/*
pub struct IdmServerReadTransaction<'a> {
pub struct IdmServerProxyReadTransaction {
// This contains read-only methods, like getting users, groups
// and other structured content.
qs: &'a QueryServer,
pub qs_read: QueryServerReadTransaction,
}
*/
pub struct IdmServerProxyWriteTransaction<'a> {
// This does NOT take any read to the memory content, allowing safe
@ -72,11 +72,11 @@ impl IdmServer {
}
}
/*
pub fn read(&self) -> IdmServerReadTransaction {
IdmServerReadTransaction { qs: &self.qs }
pub fn proxy_read(&self) -> IdmServerProxyReadTransaction {
IdmServerProxyReadTransaction {
qs_read: self.qs.read(),
}
}
*/
pub fn proxy_write(&self) -> IdmServerProxyWriteTransaction {
IdmServerProxyWriteTransaction {
@ -113,8 +113,7 @@ impl<'a> IdmServerWriteTransaction<'a> {
match &ae.step {
AuthEventStep::Init(init) => {
// Allocate a session id.
// TODO: #60 - make this new_v1 and use the tstamp.
// Allocate a session id, based on current time.
let sessionid = uuid_from_duration(ct, self.sid);
// Begin the auth procedure!
@ -161,7 +160,7 @@ impl<'a> IdmServerWriteTransaction<'a> {
// typing and functionality so we can assess what auth types can
// continue, and helps to keep non-needed entry specific data
// out of the LRU.
let account = Account::try_from_entry(entry)?;
let account = Account::try_from_entry_ro(au, entry, &qs_read)?;
let auth_session = AuthSession::new(account, init.appid.clone());
// Get the set of mechanisms that can proceed. This is tied
@ -213,11 +212,26 @@ impl<'a> IdmServerWriteTransaction<'a> {
}
}
/*
impl<'a> IdmServerReadTransaction<'a> {
pub fn whoami() -> () {}
impl IdmServerProxyReadTransaction {
pub fn get_radiusauthtoken(
&self,
au: &mut AuditScope,
rate: &RadiusAuthTokenEvent,
) -> Result<RadiusAuthToken, OperationError> {
// TODO: This needs to be an impersonate search!
let account_entry = try_audit!(
au,
self.qs_read
.impersonate_search_ext_uuid(au, &rate.target, &rate.event)
);
let account = try_audit!(
au,
RadiusAccount::try_from_entry_reduced(au, account_entry, &self.qs_read)
);
account.to_radiusauthtoken()
}
}
*/
impl<'a> IdmServerProxyWriteTransaction<'a> {
pub fn set_account_password(
@ -227,7 +241,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
) -> Result<(), OperationError> {
// Get the account
let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &pce.target));
let account = try_audit!(au, Account::try_from_entry(account_entry));
let account = try_audit!(
au,
Account::try_from_entry_rw(au, account_entry, &self.qs_write)
);
// Ask if tis all good - this step checks pwpolicy and such
// Deny the change if the account is anonymous!
@ -289,7 +306,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
) -> Result<String, OperationError> {
// Get the account
let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &gpe.target));
let account = try_audit!(au, Account::try_from_entry(account_entry));
let account = try_audit!(
au,
Account::try_from_entry_rw(au, account_entry, &self.qs_write)
);
// Ask if tis all good - this step checks pwpolicy and such
// Deny the change if the target account is anonymous!
@ -318,6 +338,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// Filter as intended (acp)
filter_all!(f_eq("uuid", PartialValue::new_uuidr(&gpe.target))),
modlist,
// Provide the event to impersonate
&gpe.event,
)
);
@ -325,6 +346,48 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
Ok(cleartext)
}
pub fn regenerate_radius_secret(
&mut self,
au: &mut AuditScope,
rrse: &RegenerateRadiusSecretEvent,
) -> Result<String, OperationError> {
// regenerates and returns the radius secret
let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &rrse.target));
let account = try_audit!(
au,
Account::try_from_entry_rw(au, account_entry, &self.qs_write)
);
// Deny the change if the target account is anonymous!
if account.is_anonymous() {
return Err(OperationError::SystemProtectedObject);
}
// Difference to the password above, this is intended to be read/copied
// by a human wiath a keyboard in some cases.
let cleartext = readable_password_from_random();
// Create a modlist from the change.
let modlist = try_audit!(au, account.regenerate_radius_secret_mod(cleartext.as_str()));
audit_log!(au, "processing change {:?}", modlist);
// Apply it.
try_audit!(
au,
self.qs_write.impersonate_modify(
au,
// Filter as executed
filter!(f_eq("uuid", PartialValue::new_uuidr(&rrse.target))),
// Filter as intended (acp)
filter_all!(f_eq("uuid", PartialValue::new_uuidr(&rrse.target))),
modlist,
// Provide the event to impersonate
&rrse.event,
)
);
Ok(cleartext)
}
pub fn commit(self, au: &mut AuditScope) -> Result<(), OperationError> {
self.qs_write.commit(au)
}
@ -337,7 +400,9 @@ mod tests {
use crate::constants::{AUTH_SESSION_TIMEOUT, UUID_ADMIN, UUID_ANONYMOUS};
use crate::credential::Credential;
use crate::event::{AuthEvent, AuthResult, ModifyEvent};
use crate::idm::event::PasswordChangeEvent;
use crate::idm::event::{
PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
};
use crate::modify::{Modify, ModifyList};
use crate::value::{PartialValue, Value};
use kanidm_proto::v1::OperationError;
@ -633,4 +698,43 @@ mod tests {
assert!(!idms_write.is_sessionid_present(&sid));
})
}
#[test]
fn test_idm_regenerate_radius_secret() {
run_idm_test!(|_qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| {
let mut idms_prox_write = idms.proxy_write();
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_ADMIN.clone());
// Generates a new credential when none exists
let r1 = idms_prox_write
.regenerate_radius_secret(au, &rrse)
.expect("Failed to reset radius credential 1");
// Regenerates and overwrites the radius credential
let r2 = idms_prox_write
.regenerate_radius_secret(au, &rrse)
.expect("Failed to reset radius credential 2");
assert!(r1 != r2);
})
}
#[test]
fn test_idm_radiusauthtoken() {
run_idm_test!(|_qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| {
let mut idms_prox_write = idms.proxy_write();
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_ADMIN.clone());
let r1 = idms_prox_write
.regenerate_radius_secret(au, &rrse)
.expect("Failed to reset radius credential 1");
idms_prox_write.commit(au).expect("failed to commit");
let idms_prox_read = idms.proxy_read();
let rate = RadiusAuthTokenEvent::new_internal(UUID_ADMIN.clone());
let tok_r = idms_prox_read
.get_radiusauthtoken(au, &rate)
.expect("Failed to generate radius auth token");
// view the token?
assert!(r1 == tok_r.secret);
})
}
}

View file

@ -1,4 +1,4 @@
#![deny(warnings)]
// #![deny(warnings)]
#![warn(unused_extern_crates)]
#[macro_use]

View file

@ -91,6 +91,10 @@ impl ModifyList<ModifyInvalid> {
Self::new_list(vec![m_purge(attr), Modify::Present(attr.to_string(), v)])
}
pub fn new_purge(attr: &str) -> Self {
Self::new_list(vec![m_purge(attr)])
}
pub fn push_mod(&mut self, modify: Modify) {
self.mods.push(modify)
}

View file

@ -214,6 +214,22 @@ impl SchemaAttribute {
}
}
fn validate_radius_utf8string(&self, v: &Value) -> Result<(), SchemaError> {
if v.is_radius_string() {
Ok(())
} else {
Err(SchemaError::InvalidAttributeSyntax)
}
}
fn validate_sshkey(&self, v: &Value) -> Result<(), SchemaError> {
if v.is_sshkey() {
Ok(())
} else {
Err(SchemaError::InvalidAttributeSyntax)
}
}
// TODO: There may be a difference between a value and a filter value on complex
// types - IE a complex type may have multiple parts that are secret, but a filter
// on that may only use a single tagged attribute for example.
@ -228,6 +244,8 @@ impl SchemaAttribute {
SyntaxType::UTF8STRING => v.is_utf8(),
SyntaxType::JSON_FILTER => v.is_json_filter(),
SyntaxType::CREDENTIAL => v.is_credential(),
SyntaxType::RADIUS_UTF8STRING => v.is_radius_string(),
SyntaxType::SSHKEY => v.is_sshkey(),
};
if r {
Ok(())
@ -322,6 +340,20 @@ impl SchemaAttribute {
acc
}
}),
SyntaxType::RADIUS_UTF8STRING => ava.iter().fold(Ok(()), |acc, v| {
if acc.is_ok() {
self.validate_radius_utf8string(v)
} else {
acc
}
}),
SyntaxType::SSHKEY => ava.iter().fold(Ok(()), |acc, v| {
if acc.is_ok() {
self.validate_sshkey(v)
} else {
acc
}
}),
}
}
}
@ -435,6 +467,14 @@ pub trait SchemaTransaction {
an.to_lowercase()
}
fn normalise_attr_if_exists(&self, an: &str) -> Option<String> {
if self.get_attributes().contains_key(an) {
Some(self.normalise_attr_name(an))
} else {
None
}
}
fn get_attributes_unique(&self) -> Vec<String> {
// This could be improved by caching this set on schema reload!
self.get_attributes()

View file

@ -295,6 +295,20 @@ pub trait QueryServerTransaction {
res
}
fn impersonate_search_ext_valid(
&self,
audit: &mut AuditScope,
f_valid: Filter<FilterValid>,
f_intent_valid: Filter<FilterValid>,
event: &Event,
) -> Result<Vec<Entry<EntryReduced, EntryCommitted>>, OperationError> {
let se = SearchEvent::new_impersonate(event, f_valid, f_intent_valid);
let mut audit_int = AuditScope::new("impersonate_search_ext");
let res = self.search_ext(&mut audit_int, &se);
audit.append_scope(audit_int);
res
}
// Who they are will go here
fn impersonate_search(
&self,
@ -312,6 +326,22 @@ pub trait QueryServerTransaction {
self.impersonate_search_valid(audit, f_valid, f_intent_valid, event)
}
fn impersonate_search_ext(
&self,
audit: &mut AuditScope,
filter: Filter<FilterInvalid>,
filter_intent: Filter<FilterInvalid>,
event: &Event,
) -> Result<Vec<Entry<EntryReduced, EntryCommitted>>, OperationError> {
let f_valid = filter
.validate(self.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?;
let f_intent_valid = filter_intent
.validate(self.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?;
self.impersonate_search_ext_valid(audit, f_valid, f_intent_valid, event)
}
// Get a single entry by it's UUID. This is heavily relied on for internal
// server operations, especially in login and acp checks for acp.
fn internal_search_uuid(
@ -340,6 +370,28 @@ pub trait QueryServerTransaction {
}
}
fn impersonate_search_ext_uuid(
&self,
audit: &mut AuditScope,
uuid: &Uuid,
event: &Event,
) -> Result<Entry<EntryReduced, EntryCommitted>, OperationError> {
let filter_intent = filter_all!(f_eq("uuid", PartialValue::new_uuid(uuid.clone())));
let filter = filter!(f_eq("uuid", PartialValue::new_uuid(uuid.clone())));
let res = self.impersonate_search_ext(audit, filter, filter_intent, event);
match res {
Ok(vs) => {
if vs.len() > 1 {
return Err(OperationError::NoMatchingEntries);
}
vs.into_iter()
.next()
.ok_or(OperationError::NoMatchingEntries)
}
Err(e) => Err(e),
}
}
/// Do a schema aware conversion from a String:String to String:Value for modification
/// present.
fn clone_value(
@ -403,6 +455,8 @@ pub trait QueryServerTransaction {
SyntaxType::JSON_FILTER => Value::new_json_filter(value)
.ok_or(OperationError::InvalidAttribute("Invalid Filter syntax".to_string())),
SyntaxType::CREDENTIAL => Err(OperationError::InvalidAttribute("Credentials can not be supplied through modification - please use the IDM api".to_string())),
SyntaxType::RADIUS_UTF8STRING => Err(OperationError::InvalidAttribute("Radius secrets can not be supplied through modification - please use the IDM api".to_string())),
SyntaxType::SSHKEY => Err(OperationError::InvalidAttribute("SSH public keys can not be supplied through modification - please use the IDM api".to_string())),
}
}
None => {
@ -475,6 +529,8 @@ pub trait QueryServerTransaction {
OperationError::InvalidAttribute("Invalid Filter syntax".to_string()),
),
SyntaxType::CREDENTIAL => Ok(PartialValue::new_credential_tag(value.as_str())),
SyntaxType::RADIUS_UTF8STRING => Ok(PartialValue::new_radius_string()),
SyntaxType::SSHKEY => Ok(PartialValue::new_sshkey_tag_s(value.as_str())),
}
}
None => {
@ -1538,6 +1594,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_SCHEMA_ATTR_MAIL,
JSON_SCHEMA_ATTR_SSH_PUBLICKEY,
JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL,
JSON_SCHEMA_ATTR_RADIUS_SECRET,
JSON_SCHEMA_CLASS_PERSON,
JSON_SCHEMA_CLASS_GROUP,
JSON_SCHEMA_CLASS_ACCOUNT,

View file

@ -25,6 +25,17 @@ pub fn password_from_random() -> String {
rand_string
}
pub fn readable_password_from_random() -> String {
let mut trng = thread_rng();
format!(
"{}-{}-{}-{}",
trng.sample_iter(&Alphanumeric).take(4).collect::<String>(),
trng.sample_iter(&Alphanumeric).take(4).collect::<String>(),
trng.sample_iter(&Alphanumeric).take(4).collect::<String>(),
trng.sample_iter(&Alphanumeric).take(4).collect::<String>(),
)
}
#[allow(dead_code)]
pub fn uuid_from_now(sid: &SID) -> Uuid {
let d = SystemTime::now()

View file

@ -1,4 +1,4 @@
use crate::be::dbvalue::{DbValueCredV1, DbValueV1};
use crate::be::dbvalue::{DbValueCredV1, DbValueTaggedStringV1, DbValueV1};
use crate::credential::Credential;
use kanidm_proto::v1::Filter as ProtoFilter;
@ -84,6 +84,8 @@ pub enum SyntaxType {
REFERENCE_UUID,
JSON_FILTER,
CREDENTIAL,
RADIUS_UTF8STRING,
SSHKEY,
}
impl TryFrom<&str> for SyntaxType {
@ -101,6 +103,8 @@ impl TryFrom<&str> for SyntaxType {
"REFERENCE_UUID" => Ok(SyntaxType::REFERENCE_UUID),
"JSON_FILTER" => Ok(SyntaxType::JSON_FILTER),
"CREDENTIAL" => Ok(SyntaxType::CREDENTIAL),
"RADIUS_UTF8STRING" => Ok(SyntaxType::RADIUS_UTF8STRING),
"SSHKEY" => Ok(SyntaxType::SSHKEY),
_ => Err(()),
}
}
@ -120,6 +124,8 @@ impl TryFrom<usize> for SyntaxType {
6 => Ok(SyntaxType::REFERENCE_UUID),
7 => Ok(SyntaxType::JSON_FILTER),
8 => Ok(SyntaxType::CREDENTIAL),
9 => Ok(SyntaxType::RADIUS_UTF8STRING),
10 => Ok(SyntaxType::SSHKEY),
_ => Err(()),
}
}
@ -137,6 +143,8 @@ impl SyntaxType {
SyntaxType::REFERENCE_UUID => "REFERENCE_UUID",
SyntaxType::JSON_FILTER => "JSON_FILTER",
SyntaxType::CREDENTIAL => "CREDENTIAL",
SyntaxType::RADIUS_UTF8STRING => "RADIUS_UTF8STRING",
SyntaxType::SSHKEY => "SSHKEY",
})
}
@ -151,6 +159,8 @@ impl SyntaxType {
SyntaxType::REFERENCE_UUID => 6,
SyntaxType::JSON_FILTER => 7,
SyntaxType::CREDENTIAL => 8,
SyntaxType::RADIUS_UTF8STRING => 9,
SyntaxType::SSHKEY => 10,
}
}
}
@ -158,8 +168,8 @@ impl SyntaxType {
#[derive(Debug, Clone)]
pub enum DataValue {
Cred(Credential),
// SshKey(String),
// RadiusCred(String),
SshKey(String),
RadiusCred(String),
}
#[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Deserialize, Serialize)]
@ -176,8 +186,8 @@ pub enum PartialValue {
JsonFilt(ProtoFilter),
// Tag, matches to a DataValue.
Cred(String),
// SshKey(String),
// RadiusCred(String),
SshKey(String),
RadiusCred,
}
impl PartialValue {
@ -337,6 +347,28 @@ impl PartialValue {
}
}
pub fn new_radius_string() -> Self {
PartialValue::RadiusCred
}
pub fn is_radius_string(&self) -> bool {
match self {
PartialValue::RadiusCred => true,
_ => false,
}
}
pub fn new_sshkey_tag_s(s: &str) -> Self {
PartialValue::SshKey(s.to_string())
}
pub fn is_sshkey(&self) -> bool {
match self {
PartialValue::SshKey(_) => true,
_ => false,
}
}
pub fn to_str(&self) -> Option<&str> {
match self {
PartialValue::Utf8(s) => Some(s.as_str()),
@ -368,6 +400,9 @@ impl PartialValue {
serde_json::to_string(s).expect("A json filter value was corrupted during run-time")
}
PartialValue::Cred(tag) => tag.to_string(),
// This will never match as we never index radius creds! See generate_idx_eq_keys
PartialValue::RadiusCred => "_".to_string(),
PartialValue::SshKey(tag) => tag.to_string(),
}
}
@ -692,6 +727,7 @@ impl Value {
PartialValue::Cred(_) => match &self.data {
Some(dv) => match dv {
DataValue::Cred(c) => Some(&c),
_ => None,
},
None => None,
},
@ -699,6 +735,40 @@ impl Value {
}
}
pub fn new_radius_str(cleartext: &str) -> Self {
Value {
pv: PartialValue::new_radius_string(),
data: Some(DataValue::RadiusCred(cleartext.to_string())),
}
}
pub fn is_radius_string(&self) -> bool {
match &self.pv {
PartialValue::RadiusCred => true,
_ => false,
}
}
pub fn get_radius_secret(&self) -> Option<&str> {
match &self.pv {
PartialValue::RadiusCred => match &self.data {
Some(dv) => match dv {
DataValue::RadiusCred(c) => Some(c.as_str()),
_ => None,
},
_ => None,
},
_ => None,
}
}
pub fn is_sshkey(&self) -> bool {
match &self.pv {
PartialValue::SshKey(_) => true,
_ => false,
}
}
pub fn contains(&self, s: &PartialValue) -> bool {
self.pv.contains(s)
}
@ -756,6 +826,14 @@ impl Value {
data: Some(DataValue::Cred(Credential::try_from(dvc.d)?)),
})
}
DbValueV1::RU(d) => Ok(Value {
pv: PartialValue::RadiusCred,
data: Some(DataValue::RadiusCred(d)),
}),
DbValueV1::SK(ts) => Ok(Value {
pv: PartialValue::SshKey(ts.t),
data: Some(DataValue::SshKey(ts.d)),
}),
}
}
@ -776,12 +854,10 @@ impl Value {
PartialValue::Cred(tag) => {
// Get the credential out and make sure it matches the type we expect.
let c = match &self.data {
Some(v) => {
match &v {
Some(v) => match &v {
DataValue::Cred(c) => c,
// _ => panic!(),
}
}
_ => panic!(),
},
None => panic!(),
};
@ -791,6 +867,29 @@ impl Value {
d: c.to_db_valuev1(),
})
}
PartialValue::RadiusCred => {
let ru = match &self.data {
Some(v) => match &v {
DataValue::RadiusCred(rc) => rc.clone(),
_ => panic!(),
},
None => panic!(),
};
DbValueV1::RU(ru)
}
PartialValue::SshKey(t) => {
let sk = match &self.data {
Some(v) => match &v {
DataValue::SshKey(sc) => sc.clone(),
_ => panic!(),
},
None => panic!(),
};
DbValueV1::SK(DbValueTaggedStringV1 {
t: t.clone(),
d: sk,
})
}
}
}
@ -876,6 +975,8 @@ impl Value {
// tag to the proto side. The credentials private data is stored seperately.
tag.to_string()
}
PartialValue::SshKey(tag) => tag.to_string(),
PartialValue::RadiusCred => "radius".to_string(),
}
}
@ -885,12 +986,24 @@ impl Value {
// data.
match &self.pv {
PartialValue::Cred(_) => match &self.data {
Some(v) => {
match &v {
Some(v) => match &v {
DataValue::Cred(_) => true,
// _ => false,
}
}
_ => false,
},
None => false,
},
PartialValue::SshKey(_) => match &self.data {
Some(v) => match &v {
DataValue::SshKey(_) => true,
_ => false,
},
None => false,
},
PartialValue::RadiusCred => match &self.data {
Some(v) => match &v {
DataValue::RadiusCred(_) => true,
_ => false,
},
None => false,
},
_ => true,
@ -909,6 +1022,8 @@ impl Value {
PartialValue::JsonFilt(s) => vec![serde_json::to_string(s)
.expect("A json filter value was corrupted during run-time")],
PartialValue::Cred(tag) => vec![tag.to_string()],
PartialValue::SshKey(tag) => vec![tag.to_string()],
PartialValue::RadiusCred => vec![],
}
}
}