mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
17 radius (#123)
Majority of radius integration and tooling complete, including docker files.
This commit is contained in:
parent
86938a7521
commit
c006341884
|
@ -2,4 +2,5 @@ target
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
test.db
|
test.db
|
||||||
|
vendor
|
||||||
|
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,3 +6,4 @@
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
test.db
|
test.db
|
||||||
/vendor
|
/vendor
|
||||||
|
kanidm_rlm_python/test_data/certs/
|
||||||
|
|
|
@ -62,12 +62,12 @@ In a new terminal, you can now build and run the client tools with:
|
||||||
|
|
||||||
cd kanidm_tools
|
cd kanidm_tools
|
||||||
cargo run -- --help
|
cargo run -- --help
|
||||||
cargo run -- whoami -H https://localhost:8080 -D anonymous -C ../insecure/ca.pem
|
cargo run -- self 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 admin -C ../insecure/ca.pem
|
||||||
|
|
||||||
For more see [getting started]
|
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
|
## Development and Testing
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,9 @@ use std::io::Read;
|
||||||
|
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest,
|
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest,
|
||||||
Entry, Filter, ModifyList, ModifyRequest, OperationError, OperationResponse, SearchRequest,
|
Entry, Filter, ModifyList, ModifyRequest, OperationError, OperationResponse, RadiusAuthToken,
|
||||||
SearchResponse, SetAuthCredential, SingleStringRequest, UserAuthToken, WhoamiResponse,
|
SearchRequest, SearchResponse, SetAuthCredential, SingleStringRequest, UserAuthToken,
|
||||||
|
WhoamiResponse,
|
||||||
};
|
};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
|
@ -155,6 +156,22 @@ impl KanidmClient {
|
||||||
Ok(r)
|
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
|
// whoami
|
||||||
// Can't use generic get due to possible un-auth case.
|
// Can't use generic get due to possible un-auth case.
|
||||||
pub fn whoami(&self) -> Result<Option<(Entry, UserAuthToken)>, ClientError> {
|
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
|
// ==== schema
|
||||||
pub fn idm_schema_list(&self) -> Result<Vec<Entry>, ClientError> {
|
pub fn idm_schema_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||||
self.perform_get_request("/v1/schema")
|
self.perform_get_request("/v1/schema")
|
||||||
|
|
|
@ -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.
|
// Test hitting all auth-required endpoints and assert they give unauthorized.
|
||||||
|
|
|
@ -139,6 +139,27 @@ impl fmt::Display for UserAuthToken {
|
||||||
// UAT will need a downcast to Entry, which adds in the claims to the entry
|
// UAT will need a downcast to Entry, which adds in the claims to the entry
|
||||||
// for the purpose of filtering.
|
// 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 ===== */
|
/* ===== low level proto types ===== */
|
||||||
|
|
||||||
// ProtoEntry vs Entry
|
// ProtoEntry vs Entry
|
||||||
|
|
36
kanidm_rlm_python/Dockerfile
Normal file
36
kanidm_rlm_python/Dockerfile
Normal 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" ]
|
25
kanidm_rlm_python/config.ini
Normal file
25
kanidm_rlm_python/config.ini
Normal 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
964
kanidm_rlm_python/default
Normal 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
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
}
|
94
kanidm_rlm_python/entrypoint.py
Normal file
94
kanidm_rlm_python/entrypoint.py
Normal 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()
|
||||||
|
|
440
kanidm_rlm_python/inner-tunnel
Normal file
440
kanidm_rlm_python/inner-tunnel
Normal 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
|
138
kanidm_rlm_python/kanidmradius.py
Normal file
138
kanidm_rlm_python/kanidmradius.py
Normal 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
|
||||||
|
|
||||||
|
|
65
kanidm_rlm_python/mod-python
Normal file
65
kanidm_rlm_python/mod-python
Normal 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
|
||||||
|
}
|
29
kanidm_rlm_python/test_data/config.ini
Normal file
29
kanidm_rlm_python/test_data/config.ini
Normal 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
|
||||||
|
|
|
@ -102,6 +102,14 @@ struct AccountCredentialSet {
|
||||||
copt: CommonOpt,
|
copt: CommonOpt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
struct AccountRadiusOpt {
|
||||||
|
#[structopt(flatten)]
|
||||||
|
aopts: AccountCommonOpt,
|
||||||
|
#[structopt(flatten)]
|
||||||
|
copt: CommonOpt,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, StructOpt)]
|
#[derive(Debug, StructOpt)]
|
||||||
enum AccountCredential {
|
enum AccountCredential {
|
||||||
#[structopt(name = "set_password")]
|
#[structopt(name = "set_password")]
|
||||||
|
@ -110,10 +118,22 @@ enum AccountCredential {
|
||||||
GeneratePassword(AccountCredentialSet),
|
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)]
|
#[derive(Debug, StructOpt)]
|
||||||
enum AccountOpt {
|
enum AccountOpt {
|
||||||
#[structopt(name = "credential")]
|
#[structopt(name = "credential")]
|
||||||
Credential(AccountCredential),
|
Credential(AccountCredential),
|
||||||
|
#[structopt(name = "radius")]
|
||||||
|
Radius(AccountRadius),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, StructOpt)]
|
#[derive(Debug, StructOpt)]
|
||||||
|
@ -274,6 +294,32 @@ fn main() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, // end AccountOpt::Credential
|
}, // 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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,5 +18,5 @@ RUN cd /etc && \
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
|
|
||||||
ENV RUST_BACKTRACE 1
|
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"]
|
||||||
|
|
|
@ -46,7 +46,7 @@ lazy_static! {
|
||||||
pub struct AccessControlSearch {
|
pub struct AccessControlSearch {
|
||||||
acp: AccessControlProfile,
|
acp: AccessControlProfile,
|
||||||
// TODO: Should this change to Value? May help to reduce transformations during processing.
|
// TODO: Should this change to Value? May help to reduce transformations during processing.
|
||||||
attrs: Vec<String>,
|
attrs: BTreeSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccessControlSearch {
|
impl AccessControlSearch {
|
||||||
|
@ -65,7 +65,7 @@ impl AccessControlSearch {
|
||||||
let attrs = try_audit!(
|
let attrs = try_audit!(
|
||||||
audit,
|
audit,
|
||||||
value
|
value
|
||||||
.get_ava_string("acp_search_attr")
|
.get_ava_set_string("acp_search_attr")
|
||||||
.ok_or(OperationError::InvalidACPState(
|
.ok_or(OperationError::InvalidACPState(
|
||||||
"Missing acp_search_attr".to_string()
|
"Missing acp_search_attr".to_string()
|
||||||
))
|
))
|
||||||
|
@ -521,9 +521,20 @@ pub trait AccessControlsTransaction {
|
||||||
// interface is beyond me ....
|
// interface is beyond me ....
|
||||||
let rec_entry: &Entry<EntryValid, EntryCommitted> = match &se.event.origin {
|
let rec_entry: &Entry<EntryValid, EntryCommitted> = match &se.event.origin {
|
||||||
EventOrigin::Internal => {
|
EventOrigin::Internal => {
|
||||||
audit_log!(audit, "IMPOSSIBLE STATE: Internal search in external interface?! Returning empty for safety.");
|
if cfg!(test) {
|
||||||
// No need to check ACS
|
audit_log!(audit, "TEST: Internal search in external interface - allowing due to cfg test ...");
|
||||||
return Ok(Vec::new());
|
// 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,
|
EventOrigin::User(e) => &e,
|
||||||
};
|
};
|
||||||
|
@ -539,8 +550,26 @@ pub trait AccessControlsTransaction {
|
||||||
let f_val = acs.acp.receiver.clone();
|
let f_val = acs.acp.receiver.clone();
|
||||||
match f_val.resolve(&se.event, None) {
|
match f_val.resolve(&se.event, None) {
|
||||||
Ok(f_res) => {
|
Ok(f_res) => {
|
||||||
|
// Is our user covered by this acs?
|
||||||
if rec_entry.entry_match_no_index(&f_res) {
|
if rec_entry.entry_match_no_index(&f_res) {
|
||||||
Some(acs)
|
// 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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -561,10 +590,11 @@ pub trait AccessControlsTransaction {
|
||||||
audit_log!(audit, "Related acs -> {:?}", racp.acp.name);
|
audit_log!(audit, "Related acs -> {:?}", racp.acp.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the set of attributes requested by the caller
|
// Build a reference set from the req_attrs
|
||||||
// TODO #69: This currently
|
let req_attrs: Option<BTreeSet<_>> = se
|
||||||
// is ALL ATTRIBUTES, so we actually work here to just remove things we
|
.attrs
|
||||||
// CAN'T see instead.
|
.as_ref()
|
||||||
|
.map(|vs| vs.iter().map(|s| s.as_str()).collect());
|
||||||
|
|
||||||
// For each entry
|
// For each entry
|
||||||
let allowed_entries: Vec<Entry<EntryReduced, EntryCommitted>> = entries
|
let allowed_entries: Vec<Entry<EntryReduced, EntryCommitted>> = entries
|
||||||
|
@ -611,12 +641,20 @@ pub trait AccessControlsTransaction {
|
||||||
})
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Remove all others that are present on the entry.
|
// Remove all others that are present on the entry.
|
||||||
audit_log!(audit, "-- for entry --> {:?}", e.get_uuid());
|
audit_log!(audit, "-- for entry --> {:?}", e.get_uuid());
|
||||||
|
audit_log!(audit, "requested attributes --> {:?}", req_attrs);
|
||||||
audit_log!(audit, "allowed attributes --> {:?}", allowed_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.
|
// Now purge the attrs that are NOT in this.
|
||||||
e.reduce_attributes(allowed_attrs)
|
e.reduce_attributes(f_allowed_attrs)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok(allowed_entries)
|
Ok(allowed_entries)
|
||||||
|
@ -1789,8 +1827,10 @@ mod tests {
|
||||||
.expect("operation failed");
|
.expect("operation failed");
|
||||||
|
|
||||||
// Help the type checker for the expect set.
|
// Help the type checker for the expect set.
|
||||||
let expect_set: Vec<Entry<EntryReduced, EntryCommitted>> =
|
let expect_set: Vec<Entry<EntryReduced, EntryCommitted>> = $expect
|
||||||
$expect.into_iter().map(|e| e.to_reduced()).collect();
|
.into_iter()
|
||||||
|
.map(|e| unsafe { e.to_reduced() })
|
||||||
|
.collect();
|
||||||
|
|
||||||
println!("expect --> {:?}", expect_set);
|
println!("expect --> {:?}", expect_set);
|
||||||
println!("result --> {:?}", reduced);
|
println!("result --> {:?}", reduced);
|
||||||
|
@ -1846,6 +1886,47 @@ mod tests {
|
||||||
test_acp_search_reduce!(&se_anon, vec![acp], r_set, ex_anon);
|
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 {
|
macro_rules! test_acp_modify {
|
||||||
(
|
(
|
||||||
$me:expr,
|
$me:expr,
|
||||||
|
|
|
@ -4,7 +4,8 @@ use crate::audit::AuditScope;
|
||||||
|
|
||||||
use crate::async_log::EventLog;
|
use crate::async_log::EventLog;
|
||||||
use crate::event::{AuthEvent, SearchEvent, SearchResult, WhoamiResult};
|
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::filter::{Filter, FilterInvalid};
|
||||||
use crate::idm::server::IdmServer;
|
use crate::idm::server::IdmServer;
|
||||||
|
@ -77,21 +78,31 @@ impl Message for SearchMessage {
|
||||||
pub struct InternalSearchMessage {
|
pub struct InternalSearchMessage {
|
||||||
pub uat: Option<UserAuthToken>,
|
pub uat: Option<UserAuthToken>,
|
||||||
pub filter: Filter<FilterInvalid>,
|
pub filter: Filter<FilterInvalid>,
|
||||||
}
|
pub attrs: Option<Vec<String>>,
|
||||||
|
|
||||||
impl InternalSearchMessage {
|
|
||||||
pub fn new(uat: Option<UserAuthToken>, filter: Filter<FilterInvalid>) -> Self {
|
|
||||||
InternalSearchMessage {
|
|
||||||
uat: uat,
|
|
||||||
filter: filter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message for InternalSearchMessage {
|
impl Message for InternalSearchMessage {
|
||||||
type Result = Result<Vec<ProtoEntry>, OperationError>;
|
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 {
|
pub struct QueryServerReadV1 {
|
||||||
|
@ -297,3 +308,103 @@ impl Handler<InternalSearchMessage> for QueryServerReadV1 {
|
||||||
res
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::async_log::EventLog;
|
||||||
use crate::event::{
|
use crate::event::{
|
||||||
CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent,
|
CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent,
|
||||||
};
|
};
|
||||||
use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent};
|
use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent, RegenerateRadiusSecretEvent};
|
||||||
use kanidm_proto::v1::OperationError;
|
use kanidm_proto::v1::OperationError;
|
||||||
|
|
||||||
use crate::idm::server::IdmServer;
|
use crate::idm::server::IdmServer;
|
||||||
|
@ -109,6 +109,36 @@ impl Message for InternalCredentialSetMessage {
|
||||||
type Result = Result<Option<String>, OperationError>;
|
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 {
|
pub struct QueryServerWriteV1 {
|
||||||
log: actix::Addr<EventLog>,
|
log: actix::Addr<EventLog>,
|
||||||
qs: QueryServer,
|
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.
|
// These below are internal only types.
|
||||||
|
|
||||||
impl Handler<PurgeTombstoneEvent> for QueryServerWriteV1 {
|
impl Handler<PurgeTombstoneEvent> for QueryServerWriteV1 {
|
||||||
|
|
|
@ -18,6 +18,12 @@ pub struct DbValueCredV1 {
|
||||||
pub d: DbCredV1,
|
pub d: DbCredV1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DbValueTaggedStringV1 {
|
||||||
|
pub t: String,
|
||||||
|
pub d: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub enum DbValueV1 {
|
pub enum DbValueV1 {
|
||||||
U8(String),
|
U8(String),
|
||||||
|
@ -29,4 +35,6 @@ pub enum DbValueV1 {
|
||||||
RF(Uuid),
|
RF(Uuid),
|
||||||
JF(String),
|
JF(String),
|
||||||
CR(DbValueCredV1),
|
CR(DbValueCredV1),
|
||||||
|
RU(String),
|
||||||
|
SK(DbValueTaggedStringV1),
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,6 +129,8 @@ pub trait BackendTransaction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FilterResolved::Or(l) => {
|
FilterResolved::Or(l) => {
|
||||||
|
// Importantly if this has no inner elements, this returns
|
||||||
|
// an empty list.
|
||||||
let mut result = IDLBitRange::new();
|
let mut result = IDLBitRange::new();
|
||||||
let mut partial = false;
|
let mut partial = false;
|
||||||
// For each filter in l
|
// For each filter in l
|
||||||
|
@ -1896,6 +1898,31 @@ mod tests {
|
||||||
panic!("");
|
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!("");
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -356,6 +356,7 @@ pub static JSON_IDM_SELF_ACP_READ_V1: &'static str = r#"{
|
||||||
"class",
|
"class",
|
||||||
"memberof",
|
"memberof",
|
||||||
"member",
|
"member",
|
||||||
|
"radius_secret",
|
||||||
"uuid"
|
"uuid"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -367,7 +368,7 @@ pub static JSON_IDM_SELF_ACP_WRITE_V1: &'static str = r#"{
|
||||||
"class": ["object", "access_control_profile", "access_control_modify"],
|
"class": ["object", "access_control_profile", "access_control_modify"],
|
||||||
"name": ["idm_self_acp_write"],
|
"name": ["idm_self_acp_write"],
|
||||||
"uuid": ["00000000-0000-0000-0000-ffffff000021"],
|
"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_enable": ["true"],
|
||||||
"acp_receiver": [
|
"acp_receiver": [
|
||||||
"{\"And\": [\"Self\", {\"AndNot\": {\"Or\": [{\"Eq\": [\"class\", \"tombstone\"]}, {\"Eq\": [\"class\", \"recycled\"]}, {\"Eq\": [\"uuid\", \"00000000-0000-0000-0000-ffffffffffff\"]}]}}]}"
|
"{\"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\""
|
"\"Self\""
|
||||||
],
|
],
|
||||||
"acp_modify_removedattr": [
|
"acp_modify_removedattr": [
|
||||||
"name", "displayname", "legalname"
|
"name", "displayname", "legalname", "radius_secret", "primary_credential"
|
||||||
],
|
],
|
||||||
"acp_modify_presentattr": [
|
"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";
|
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
|
// 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.
|
// 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#"{
|
pub static JSON_IDM_ACP_RADIUS_SERVERS_V1: &'static str = r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": [
|
"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\"]}]}}]}"
|
"{\"And\": [{\"Pres\": \"class\"}, {\"AndNot\": {\"Or\": [{\"Eq\": [\"class\", \"tombstone\"]}, {\"Eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||||
],
|
],
|
||||||
"acp_search_attr": [
|
"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"
|
"ssh_publickey"
|
||||||
],
|
],
|
||||||
"syntax": [
|
"syntax": [
|
||||||
"UTF8STRING"
|
"SSHKEY"
|
||||||
],
|
],
|
||||||
"uuid": [
|
"uuid": [
|
||||||
"00000000-0000-0000-0000-ffff00000042"
|
"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 UUID_SCHEMA_CLASS_PERSON: &'static str = "00000000-0000-0000-0000-ffff00000044";
|
||||||
pub static JSON_SCHEMA_CLASS_PERSON: &'static str = r#"
|
pub static JSON_SCHEMA_CLASS_PERSON: &'static str = r#"
|
||||||
|
@ -1336,7 +1365,8 @@ pub static JSON_SCHEMA_CLASS_ACCOUNT: &'static str = r#"
|
||||||
],
|
],
|
||||||
"systemmay": [
|
"systemmay": [
|
||||||
"primary_credential",
|
"primary_credential",
|
||||||
"ssh_publickey"
|
"ssh_publickey",
|
||||||
|
"radius_secret"
|
||||||
],
|
],
|
||||||
"systemmust": [
|
"systemmust": [
|
||||||
"displayname",
|
"displayname",
|
||||||
|
|
|
@ -15,11 +15,14 @@ use crate::config::Configuration;
|
||||||
|
|
||||||
// SearchResult
|
// SearchResult
|
||||||
use crate::actors::v1_read::QueryServerReadV1;
|
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::QueryServerWriteV1;
|
||||||
use crate::actors::v1_write::{
|
use crate::actors::v1_write::{
|
||||||
CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage, InternalCredentialSetMessage,
|
CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage, InternalCredentialSetMessage,
|
||||||
ModifyMessage,
|
InternalRegenerateRadiusMessage, ModifyMessage, PurgeAttributeMessage,
|
||||||
};
|
};
|
||||||
use crate::async_log;
|
use crate::async_log;
|
||||||
use crate::audit::AuditScope;
|
use crate::audit::AuditScope;
|
||||||
|
@ -187,12 +190,17 @@ fn json_rest_event_get(
|
||||||
req: HttpRequest<AppState>,
|
req: HttpRequest<AppState>,
|
||||||
state: State<AppState>,
|
state: State<AppState>,
|
||||||
filter: Filter<FilterInvalid>,
|
filter: Filter<FilterInvalid>,
|
||||||
|
attrs: Option<Vec<String>>,
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||||
let uat = get_current_user(&req);
|
let uat = get_current_user(&req);
|
||||||
|
|
||||||
// TODO: I think we'll need to change this to take an internal filter
|
// TODO: I think we'll need to change this to take an internal filter
|
||||||
// type that we send to the qs.
|
// 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 {
|
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
|
||||||
Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)),
|
Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)),
|
||||||
|
@ -207,12 +215,17 @@ fn json_rest_event_get_id(
|
||||||
req: HttpRequest<AppState>,
|
req: HttpRequest<AppState>,
|
||||||
state: State<AppState>,
|
state: State<AppState>,
|
||||||
filter: Filter<FilterInvalid>,
|
filter: Filter<FilterInvalid>,
|
||||||
|
attrs: Option<Vec<String>>,
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||||
let uat = get_current_user(&req);
|
let uat = get_current_user(&req);
|
||||||
|
|
||||||
let filter = Filter::join_parts_and(filter, filter_all!(f_id(path.as_str())));
|
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 {
|
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
|
||||||
Ok(mut event_result) => {
|
Ok(mut event_result) => {
|
||||||
|
@ -225,6 +238,71 @@ fn json_rest_event_get_id(
|
||||||
Box::new(res)
|
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(
|
fn json_rest_event_credential_put(
|
||||||
id: String,
|
id: String,
|
||||||
cred_id: Option<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("attributetype")),
|
||||||
f_eq("class", PartialValue::new_class("classtype"))
|
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(
|
fn schema_attributetype_get(
|
||||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("attributetype")));
|
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(
|
fn schema_attributetype_get_id(
|
||||||
|
@ -320,7 +398,11 @@ fn schema_attributetype_get_id(
|
||||||
f_eq("attributename", PartialValue::new_iutf8s(path.as_str()))
|
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 {
|
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
|
||||||
Ok(mut event_result) => {
|
Ok(mut event_result) => {
|
||||||
|
@ -337,7 +419,7 @@ fn schema_classtype_get(
|
||||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("classtype")));
|
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(
|
fn schema_classtype_get_id(
|
||||||
|
@ -351,7 +433,11 @@ fn schema_classtype_get_id(
|
||||||
f_eq("classname", PartialValue::new_iutf8s(path.as_str()))
|
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 {
|
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
|
||||||
Ok(mut event_result) => {
|
Ok(mut event_result) => {
|
||||||
|
@ -368,14 +454,14 @@ fn account_get(
|
||||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
|
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(
|
fn account_get_id(
|
||||||
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
|
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
|
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(
|
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)
|
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(
|
fn group_get(
|
||||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
|
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(
|
fn group_id_get(
|
||||||
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
|
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
|
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 {
|
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?
|
// Can we self lock?
|
||||||
})
|
})
|
||||||
.resource("/v1/self/_radius", |r| {
|
.resource("/v1/self/_radius", |r| {
|
||||||
|
// Get our radius secret for manual configuration
|
||||||
r.method(http::Method::GET).with(do_nothing)
|
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| {
|
.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)
|
r.method(http::Method::POST).with(do_nothing)
|
||||||
})
|
})
|
||||||
.resource("/v1/self/_radius/_config/{secret_otp}", |r| {
|
.resource("/v1/self/_radius/_config/{secret_otp}", |r| {
|
||||||
|
@ -1019,11 +1184,17 @@ pub fn create_server_core(config: Configuration) {
|
||||||
// add post, delete
|
// add post, delete
|
||||||
})
|
})
|
||||||
.resource("/v1/account/{id}/_radius", |r| {
|
.resource("/v1/account/{id}/_radius", |r| {
|
||||||
r.method(http::Method::GET).with(do_nothing)
|
r.method(http::Method::GET)
|
||||||
// more to be added
|
.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| {
|
.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
|
// Groups
|
||||||
.resource("/v1/group", |r| {
|
.resource("/v1/group", |r| {
|
||||||
|
|
|
@ -162,7 +162,9 @@ pub struct EntryInvalid;
|
||||||
// pub struct EntryNormalised;
|
// pub struct EntryNormalised;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct EntryReduced;
|
pub struct EntryReduced {
|
||||||
|
uuid: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Entry<VALID, STATE> {
|
pub struct Entry<VALID, STATE> {
|
||||||
|
@ -980,10 +982,11 @@ impl Entry<EntryValid, EntryCommitted> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
pub unsafe fn to_reduced(self) -> Entry<EntryReduced, EntryCommitted> {
|
||||||
pub fn to_reduced(self) -> Entry<EntryReduced, EntryCommitted> {
|
|
||||||
Entry {
|
Entry {
|
||||||
valid: EntryReduced,
|
valid: EntryReduced {
|
||||||
|
uuid: self.valid.uuid,
|
||||||
|
},
|
||||||
state: self.state,
|
state: self.state,
|
||||||
attrs: self.attrs,
|
attrs: self.attrs,
|
||||||
}
|
}
|
||||||
|
@ -996,7 +999,7 @@ impl Entry<EntryValid, EntryCommitted> {
|
||||||
// Remove all attrs from our tree that are NOT in the allowed set.
|
// Remove all attrs from our tree that are NOT in the allowed set.
|
||||||
|
|
||||||
let Entry {
|
let Entry {
|
||||||
valid: _s_valid,
|
valid: s_valid,
|
||||||
state: s_state,
|
state: s_state,
|
||||||
attrs: s_attrs,
|
attrs: s_attrs,
|
||||||
} = self;
|
} = self;
|
||||||
|
@ -1013,135 +1016,11 @@ impl Entry<EntryValid, EntryCommitted> {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Entry {
|
Entry {
|
||||||
valid: EntryReduced,
|
valid: EntryReduced { uuid: s_valid.uuid },
|
||||||
state: s_state,
|
state: s_state,
|
||||||
attrs: f_attrs,
|
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> {
|
impl<STATE> Entry<EntryValid, STATE> {
|
||||||
|
@ -1244,6 +1123,10 @@ impl<STATE> Entry<EntryValid, STATE> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entry<EntryReduced, EntryCommitted> {
|
impl Entry<EntryReduced, EntryCommitted> {
|
||||||
|
pub fn get_uuid(&self) -> &Uuid {
|
||||||
|
&self.valid.uuid
|
||||||
|
}
|
||||||
|
|
||||||
pub fn into_pe(
|
pub fn into_pe(
|
||||||
&self,
|
&self,
|
||||||
audit: &mut AuditScope,
|
audit: &mut AuditScope,
|
||||||
|
@ -1316,6 +1199,150 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
||||||
r
|
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 {
|
pub fn attribute_pres(&self, attr: &str) -> bool {
|
||||||
// Note, we don't normalise attr name, but I think that's not
|
// Note, we don't normalise attr name, but I think that's not
|
||||||
// something we should over-optimise on.
|
// something we should over-optimise on.
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::audit::AuditScope;
|
use crate::audit::AuditScope;
|
||||||
use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, EntryValid};
|
use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, EntryValid};
|
||||||
use crate::filter::{Filter, FilterValid};
|
use crate::filter::{Filter, FilterValid};
|
||||||
|
use crate::schema::SchemaTransaction;
|
||||||
|
use crate::value::PartialValue;
|
||||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
AuthCredential, AuthResponse, AuthState, AuthStep, SearchResponse, UserAuthToken,
|
AuthCredential, AuthResponse, AuthState, AuthStep, SearchResponse, UserAuthToken,
|
||||||
|
@ -25,6 +27,7 @@ use crate::filter::FilterInvalid;
|
||||||
use crate::modify::ModifyInvalid;
|
use crate::modify::ModifyInvalid;
|
||||||
|
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -217,7 +220,7 @@ pub struct SearchEvent {
|
||||||
pub filter: Filter<FilterValid>,
|
pub filter: Filter<FilterValid>,
|
||||||
// This is the original filter, for the purpose of ACI checking.
|
// This is the original filter, for the purpose of ACI checking.
|
||||||
pub filter_orig: Filter<FilterValid>,
|
pub filter_orig: Filter<FilterValid>,
|
||||||
// TODO #83: Add list of attributes to request
|
pub attrs: Option<BTreeSet<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SearchEvent {
|
impl SearchEvent {
|
||||||
|
@ -239,6 +242,9 @@ impl SearchEvent {
|
||||||
filter_orig: f
|
filter_orig: f
|
||||||
.validate(qs.get_schema())
|
.validate(qs.get_schema())
|
||||||
.map_err(|e| OperationError::SchemaViolation(e))?,
|
.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),
|
Err(e) => Err(e),
|
||||||
}
|
}
|
||||||
|
@ -249,6 +255,21 @@ impl SearchEvent {
|
||||||
msg: InternalSearchMessage,
|
msg: InternalSearchMessage,
|
||||||
qs: &QueryServerReadTransaction,
|
qs: &QueryServerReadTransaction,
|
||||||
) -> Result<Self, OperationError> {
|
) -> 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 {
|
Ok(SearchEvent {
|
||||||
event: Event::from_ro_uat(audit, qs, msg.uat)?,
|
event: Event::from_ro_uat(audit, qs, msg.uat)?,
|
||||||
// We do need to do this twice to account for the ignore_hidden
|
// We do need to do this twice to account for the ignore_hidden
|
||||||
|
@ -263,6 +284,7 @@ impl SearchEvent {
|
||||||
.filter
|
.filter
|
||||||
.validate(qs.get_schema())
|
.validate(qs.get_schema())
|
||||||
.map_err(|e| OperationError::SchemaViolation(e))?,
|
.map_err(|e| OperationError::SchemaViolation(e))?,
|
||||||
|
attrs: r_attrs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,6 +301,26 @@ impl SearchEvent {
|
||||||
filter_orig: filter_all!(f_self())
|
filter_orig: filter_all!(f_self())
|
||||||
.validate(qs.get_schema())
|
.validate(qs.get_schema())
|
||||||
.map_err(|e| OperationError::SchemaViolation(e))?,
|
.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),
|
event: Event::from_impersonate_entry_ser(e),
|
||||||
filter: filter.clone().to_valid(),
|
filter: filter.clone().to_valid(),
|
||||||
filter_orig: filter.to_valid(),
|
filter_orig: filter.to_valid(),
|
||||||
|
attrs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,6 +344,7 @@ impl SearchEvent {
|
||||||
event: Event::from_impersonate_entry(e),
|
event: Event::from_impersonate_entry(e),
|
||||||
filter: filter.clone().to_valid(),
|
filter: filter.clone().to_valid(),
|
||||||
filter_orig: filter.to_valid(),
|
filter_orig: filter.to_valid(),
|
||||||
|
attrs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,6 +357,7 @@ impl SearchEvent {
|
||||||
event: Event::from_impersonate(event),
|
event: Event::from_impersonate(event),
|
||||||
filter: filter,
|
filter: filter,
|
||||||
filter_orig: filter_orig,
|
filter_orig: filter_orig,
|
||||||
|
attrs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,6 +396,7 @@ impl SearchEvent {
|
||||||
event: Event::from_impersonate_entry(e),
|
event: Event::from_impersonate_entry(e),
|
||||||
filter: filter.clone().to_recycled().to_valid(),
|
filter: filter.clone().to_recycled().to_valid(),
|
||||||
filter_orig: filter.to_valid(),
|
filter_orig: filter.to_valid(),
|
||||||
|
attrs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,6 +410,7 @@ impl SearchEvent {
|
||||||
event: Event::from_impersonate_entry(e),
|
event: Event::from_impersonate_entry(e),
|
||||||
filter: filter.clone().to_ignore_hidden().to_valid(),
|
filter: filter.clone().to_ignore_hidden().to_valid(),
|
||||||
filter_orig: filter.to_valid(),
|
filter_orig: filter.to_valid(),
|
||||||
|
attrs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,6 +420,7 @@ impl SearchEvent {
|
||||||
event: Event::from_internal(),
|
event: Event::from_internal(),
|
||||||
filter: filter.clone().to_valid(),
|
filter: filter.clone().to_valid(),
|
||||||
filter_orig: filter.to_valid(),
|
filter_orig: filter.to_valid(),
|
||||||
|
attrs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,6 +429,7 @@ impl SearchEvent {
|
||||||
event: Event::from_internal(),
|
event: Event::from_internal(),
|
||||||
filter: filter.clone(),
|
filter: filter.clone(),
|
||||||
filter_orig: filter,
|
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 {
|
pub fn new_internal(filter: Filter<FilterValid>, modlist: ModifyList<ModifyValid>) -> Self {
|
||||||
ModifyEvent {
|
ModifyEvent {
|
||||||
event: Event::from_internal(),
|
event: Event::from_internal(),
|
||||||
|
|
|
@ -3,11 +3,13 @@ use kanidm_proto::v1::OperationError;
|
||||||
|
|
||||||
use kanidm_proto::v1::UserAuthToken;
|
use kanidm_proto::v1::UserAuthToken;
|
||||||
|
|
||||||
|
use crate::audit::AuditScope;
|
||||||
use crate::constants::UUID_ANONYMOUS;
|
use crate::constants::UUID_ANONYMOUS;
|
||||||
use crate::credential::Credential;
|
use crate::credential::Credential;
|
||||||
use crate::idm::claim::Claim;
|
use crate::idm::claim::Claim;
|
||||||
use crate::idm::group::Group;
|
use crate::idm::group::Group;
|
||||||
use crate::modify::{ModifyInvalid, ModifyList};
|
use crate::modify::{ModifyInvalid, ModifyList};
|
||||||
|
use crate::server::{QueryServerReadTransaction, QueryServerWriteTransaction};
|
||||||
use crate::value::{PartialValue, Value};
|
use crate::value::{PartialValue, Value};
|
||||||
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -16,6 +18,46 @@ lazy_static! {
|
||||||
static ref PVCLASS_ACCOUNT: PartialValue = PartialValue::new_class("account");
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct Account {
|
pub(crate) struct Account {
|
||||||
// Later these could be &str if we cache entry here too ...
|
// Later these could be &str if we cache entry here too ...
|
||||||
|
@ -35,45 +77,29 @@ pub(crate) struct Account {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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_ro(
|
||||||
pub(crate) fn try_from_entry(
|
au: &mut AuditScope,
|
||||||
|
value: Entry<EntryValid, EntryCommitted>,
|
||||||
|
qs: &QueryServerReadTransaction,
|
||||||
|
) -> Result<Self, OperationError> {
|
||||||
|
let groups = Group::try_from_account_entry_ro(au, &value, qs)?;
|
||||||
|
try_from_entry!(value, groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn try_from_entry_no_groups(
|
||||||
value: Entry<EntryValid, EntryCommitted>,
|
value: Entry<EntryValid, EntryCommitted>,
|
||||||
) -> Result<Self, OperationError> {
|
) -> Result<Self, OperationError> {
|
||||||
// Check the classes
|
try_from_entry!(value, vec![])
|
||||||
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());
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Could this actually take a claims list and application instead?
|
// Could this actually take a claims list and application instead?
|
||||||
|
@ -127,6 +153,14 @@ impl Account {
|
||||||
} // no appid
|
} // 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" ...
|
// 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() };
|
unsafe { Entry::unsafe_from_entry_str(JSON_ANONYMOUS_V1).to_valid_new() };
|
||||||
let anon_e = unsafe { anon_e.to_valid_committed() };
|
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);
|
println!("{:?}", anon_account);
|
||||||
// I think that's it? we may want to check anonymous mech ...
|
// I think that's it? we may want to check anonymous mech ...
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::actors::v1_write::IdmAccountSetPasswordMessage;
|
use crate::actors::v1_write::IdmAccountSetPasswordMessage;
|
||||||
use crate::audit::AuditScope;
|
use crate::audit::AuditScope;
|
||||||
use crate::event::Event;
|
use crate::event::Event;
|
||||||
use crate::server::QueryServerWriteTransaction;
|
use crate::server::{QueryServerReadTransaction, QueryServerWriteTransaction};
|
||||||
|
|
||||||
use uuid::Uuid;
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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::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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Group {
|
pub struct Group {
|
||||||
// name
|
name: String,
|
||||||
// uuid
|
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 {
|
impl Group {
|
||||||
/*
|
pub fn try_from_account_entry_red_ro(
|
||||||
pub fn new() -> Self {
|
au: &mut AuditScope,
|
||||||
Group {}
|
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 {
|
pub fn into_proto(&self) -> ProtoGroup {
|
||||||
unimplemented!();
|
ProtoGroup {
|
||||||
|
name: self.name.clone(),
|
||||||
|
uuid: self.uuid.to_hyphenated_ref().to_string(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ macro_rules! entry_str_to_account {
|
||||||
unsafe { Entry::unsafe_from_entry_str($entry_str).to_valid_new() };
|
unsafe { Entry::unsafe_from_entry_str($entry_str).to_valid_new() };
|
||||||
let e = unsafe { e.to_valid_committed() };
|
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")
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,5 +6,6 @@ pub(crate) mod authsession;
|
||||||
pub(crate) mod claim;
|
pub(crate) mod claim;
|
||||||
pub(crate) mod event;
|
pub(crate) mod event;
|
||||||
pub(crate) mod group;
|
pub(crate) mod group;
|
||||||
|
pub(crate) mod radius;
|
||||||
pub(crate) mod server;
|
pub(crate) mod server;
|
||||||
// mod identity;
|
// mod identity;
|
||||||
|
|
78
kanidmd/src/lib/idm/radius.rs
Normal file
78
kanidmd/src/lib/idm/radius.rs
Normal 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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,13 +3,18 @@ use crate::constants::AUTH_SESSION_TIMEOUT;
|
||||||
use crate::event::{AuthEvent, AuthEventStep, AuthResult};
|
use crate::event::{AuthEvent, AuthEventStep, AuthResult};
|
||||||
use crate::idm::account::Account;
|
use crate::idm::account::Account;
|
||||||
use crate::idm::authsession::AuthSession;
|
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::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 crate::value::PartialValue;
|
||||||
|
|
||||||
use kanidm_proto::v1::AuthState;
|
use kanidm_proto::v1::AuthState;
|
||||||
use kanidm_proto::v1::OperationError;
|
use kanidm_proto::v1::OperationError;
|
||||||
|
use kanidm_proto::v1::RadiusAuthToken;
|
||||||
|
|
||||||
use concread::cowcell::{CowCell, CowCellWriteTxn};
|
use concread::cowcell::{CowCell, CowCellWriteTxn};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
@ -21,9 +26,6 @@ pub struct IdmServer {
|
||||||
// means that limits to sessions can be easily applied and checked to
|
// 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
|
// variaous accounts, and we have a good idea of how to structure the
|
||||||
// in memory caches related to locking.
|
// 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>>,
|
sessions: CowCell<BTreeMap<Uuid, AuthSession>>,
|
||||||
// Need a reference to the query server.
|
// Need a reference to the query server.
|
||||||
qs: QueryServer,
|
qs: QueryServer,
|
||||||
|
@ -40,13 +42,11 @@ pub struct IdmServerWriteTransaction<'a> {
|
||||||
sid: &'a SID,
|
sid: &'a SID,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
pub struct IdmServerProxyReadTransaction {
|
||||||
pub struct IdmServerReadTransaction<'a> {
|
|
||||||
// This contains read-only methods, like getting users, groups
|
// This contains read-only methods, like getting users, groups
|
||||||
// and other structured content.
|
// and other structured content.
|
||||||
qs: &'a QueryServer,
|
pub qs_read: QueryServerReadTransaction,
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
pub struct IdmServerProxyWriteTransaction<'a> {
|
pub struct IdmServerProxyWriteTransaction<'a> {
|
||||||
// This does NOT take any read to the memory content, allowing safe
|
// This does NOT take any read to the memory content, allowing safe
|
||||||
|
@ -72,11 +72,11 @@ impl IdmServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
pub fn proxy_read(&self) -> IdmServerProxyReadTransaction {
|
||||||
pub fn read(&self) -> IdmServerReadTransaction {
|
IdmServerProxyReadTransaction {
|
||||||
IdmServerReadTransaction { qs: &self.qs }
|
qs_read: self.qs.read(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
pub fn proxy_write(&self) -> IdmServerProxyWriteTransaction {
|
pub fn proxy_write(&self) -> IdmServerProxyWriteTransaction {
|
||||||
IdmServerProxyWriteTransaction {
|
IdmServerProxyWriteTransaction {
|
||||||
|
@ -113,8 +113,7 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
||||||
|
|
||||||
match &ae.step {
|
match &ae.step {
|
||||||
AuthEventStep::Init(init) => {
|
AuthEventStep::Init(init) => {
|
||||||
// Allocate a session id.
|
// Allocate a session id, based on current time.
|
||||||
// TODO: #60 - make this new_v1 and use the tstamp.
|
|
||||||
let sessionid = uuid_from_duration(ct, self.sid);
|
let sessionid = uuid_from_duration(ct, self.sid);
|
||||||
|
|
||||||
// Begin the auth procedure!
|
// Begin the auth procedure!
|
||||||
|
@ -161,7 +160,7 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
||||||
// typing and functionality so we can assess what auth types can
|
// typing and functionality so we can assess what auth types can
|
||||||
// continue, and helps to keep non-needed entry specific data
|
// continue, and helps to keep non-needed entry specific data
|
||||||
// out of the LRU.
|
// 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());
|
let auth_session = AuthSession::new(account, init.appid.clone());
|
||||||
|
|
||||||
// Get the set of mechanisms that can proceed. This is tied
|
// Get the set of mechanisms that can proceed. This is tied
|
||||||
|
@ -213,11 +212,26 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
impl IdmServerProxyReadTransaction {
|
||||||
impl<'a> IdmServerReadTransaction<'a> {
|
pub fn get_radiusauthtoken(
|
||||||
pub fn whoami() -> () {}
|
&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> {
|
impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
pub fn set_account_password(
|
pub fn set_account_password(
|
||||||
|
@ -227,7 +241,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
// Get the account
|
// Get the account
|
||||||
let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &pce.target));
|
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
|
// Ask if tis all good - this step checks pwpolicy and such
|
||||||
|
|
||||||
// Deny the change if the account is anonymous!
|
// Deny the change if the account is anonymous!
|
||||||
|
@ -289,7 +306,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
) -> Result<String, OperationError> {
|
) -> Result<String, OperationError> {
|
||||||
// Get the account
|
// Get the account
|
||||||
let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &gpe.target));
|
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
|
// Ask if tis all good - this step checks pwpolicy and such
|
||||||
|
|
||||||
// Deny the change if the target account is anonymous!
|
// Deny the change if the target account is anonymous!
|
||||||
|
@ -318,6 +338,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
// Filter as intended (acp)
|
// Filter as intended (acp)
|
||||||
filter_all!(f_eq("uuid", PartialValue::new_uuidr(&gpe.target))),
|
filter_all!(f_eq("uuid", PartialValue::new_uuidr(&gpe.target))),
|
||||||
modlist,
|
modlist,
|
||||||
|
// Provide the event to impersonate
|
||||||
&gpe.event,
|
&gpe.event,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -325,6 +346,48 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
Ok(cleartext)
|
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> {
|
pub fn commit(self, au: &mut AuditScope) -> Result<(), OperationError> {
|
||||||
self.qs_write.commit(au)
|
self.qs_write.commit(au)
|
||||||
}
|
}
|
||||||
|
@ -337,7 +400,9 @@ mod tests {
|
||||||
use crate::constants::{AUTH_SESSION_TIMEOUT, UUID_ADMIN, UUID_ANONYMOUS};
|
use crate::constants::{AUTH_SESSION_TIMEOUT, UUID_ADMIN, UUID_ANONYMOUS};
|
||||||
use crate::credential::Credential;
|
use crate::credential::Credential;
|
||||||
use crate::event::{AuthEvent, AuthResult, ModifyEvent};
|
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::modify::{Modify, ModifyList};
|
||||||
use crate::value::{PartialValue, Value};
|
use crate::value::{PartialValue, Value};
|
||||||
use kanidm_proto::v1::OperationError;
|
use kanidm_proto::v1::OperationError;
|
||||||
|
@ -633,4 +698,43 @@ mod tests {
|
||||||
assert!(!idms_write.is_sessionid_present(&sid));
|
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);
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#![deny(warnings)]
|
// #![deny(warnings)]
|
||||||
#![warn(unused_extern_crates)]
|
#![warn(unused_extern_crates)]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|
|
@ -91,6 +91,10 @@ impl ModifyList<ModifyInvalid> {
|
||||||
Self::new_list(vec![m_purge(attr), Modify::Present(attr.to_string(), v)])
|
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) {
|
pub fn push_mod(&mut self, modify: Modify) {
|
||||||
self.mods.push(modify)
|
self.mods.push(modify)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// 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
|
// 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.
|
// on that may only use a single tagged attribute for example.
|
||||||
|
@ -228,6 +244,8 @@ impl SchemaAttribute {
|
||||||
SyntaxType::UTF8STRING => v.is_utf8(),
|
SyntaxType::UTF8STRING => v.is_utf8(),
|
||||||
SyntaxType::JSON_FILTER => v.is_json_filter(),
|
SyntaxType::JSON_FILTER => v.is_json_filter(),
|
||||||
SyntaxType::CREDENTIAL => v.is_credential(),
|
SyntaxType::CREDENTIAL => v.is_credential(),
|
||||||
|
SyntaxType::RADIUS_UTF8STRING => v.is_radius_string(),
|
||||||
|
SyntaxType::SSHKEY => v.is_sshkey(),
|
||||||
};
|
};
|
||||||
if r {
|
if r {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -322,6 +340,20 @@ impl SchemaAttribute {
|
||||||
acc
|
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()
|
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> {
|
fn get_attributes_unique(&self) -> Vec<String> {
|
||||||
// This could be improved by caching this set on schema reload!
|
// This could be improved by caching this set on schema reload!
|
||||||
self.get_attributes()
|
self.get_attributes()
|
||||||
|
|
|
@ -295,6 +295,20 @@ pub trait QueryServerTransaction {
|
||||||
res
|
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
|
// Who they are will go here
|
||||||
fn impersonate_search(
|
fn impersonate_search(
|
||||||
&self,
|
&self,
|
||||||
|
@ -312,6 +326,22 @@ pub trait QueryServerTransaction {
|
||||||
self.impersonate_search_valid(audit, f_valid, f_intent_valid, event)
|
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
|
// 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.
|
// server operations, especially in login and acp checks for acp.
|
||||||
fn internal_search_uuid(
|
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
|
/// Do a schema aware conversion from a String:String to String:Value for modification
|
||||||
/// present.
|
/// present.
|
||||||
fn clone_value(
|
fn clone_value(
|
||||||
|
@ -403,6 +455,8 @@ pub trait QueryServerTransaction {
|
||||||
SyntaxType::JSON_FILTER => Value::new_json_filter(value)
|
SyntaxType::JSON_FILTER => Value::new_json_filter(value)
|
||||||
.ok_or(OperationError::InvalidAttribute("Invalid Filter syntax".to_string())),
|
.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::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 => {
|
None => {
|
||||||
|
@ -475,6 +529,8 @@ pub trait QueryServerTransaction {
|
||||||
OperationError::InvalidAttribute("Invalid Filter syntax".to_string()),
|
OperationError::InvalidAttribute("Invalid Filter syntax".to_string()),
|
||||||
),
|
),
|
||||||
SyntaxType::CREDENTIAL => Ok(PartialValue::new_credential_tag(value.as_str())),
|
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 => {
|
None => {
|
||||||
|
@ -1538,6 +1594,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
JSON_SCHEMA_ATTR_MAIL,
|
JSON_SCHEMA_ATTR_MAIL,
|
||||||
JSON_SCHEMA_ATTR_SSH_PUBLICKEY,
|
JSON_SCHEMA_ATTR_SSH_PUBLICKEY,
|
||||||
JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL,
|
JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL,
|
||||||
|
JSON_SCHEMA_ATTR_RADIUS_SECRET,
|
||||||
JSON_SCHEMA_CLASS_PERSON,
|
JSON_SCHEMA_CLASS_PERSON,
|
||||||
JSON_SCHEMA_CLASS_GROUP,
|
JSON_SCHEMA_CLASS_GROUP,
|
||||||
JSON_SCHEMA_CLASS_ACCOUNT,
|
JSON_SCHEMA_CLASS_ACCOUNT,
|
||||||
|
|
|
@ -25,6 +25,17 @@ pub fn password_from_random() -> String {
|
||||||
rand_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)]
|
#[allow(dead_code)]
|
||||||
pub fn uuid_from_now(sid: &SID) -> Uuid {
|
pub fn uuid_from_now(sid: &SID) -> Uuid {
|
||||||
let d = SystemTime::now()
|
let d = SystemTime::now()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::be::dbvalue::{DbValueCredV1, DbValueV1};
|
use crate::be::dbvalue::{DbValueCredV1, DbValueTaggedStringV1, DbValueV1};
|
||||||
use crate::credential::Credential;
|
use crate::credential::Credential;
|
||||||
use kanidm_proto::v1::Filter as ProtoFilter;
|
use kanidm_proto::v1::Filter as ProtoFilter;
|
||||||
|
|
||||||
|
@ -84,6 +84,8 @@ pub enum SyntaxType {
|
||||||
REFERENCE_UUID,
|
REFERENCE_UUID,
|
||||||
JSON_FILTER,
|
JSON_FILTER,
|
||||||
CREDENTIAL,
|
CREDENTIAL,
|
||||||
|
RADIUS_UTF8STRING,
|
||||||
|
SSHKEY,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for SyntaxType {
|
impl TryFrom<&str> for SyntaxType {
|
||||||
|
@ -101,6 +103,8 @@ impl TryFrom<&str> for SyntaxType {
|
||||||
"REFERENCE_UUID" => Ok(SyntaxType::REFERENCE_UUID),
|
"REFERENCE_UUID" => Ok(SyntaxType::REFERENCE_UUID),
|
||||||
"JSON_FILTER" => Ok(SyntaxType::JSON_FILTER),
|
"JSON_FILTER" => Ok(SyntaxType::JSON_FILTER),
|
||||||
"CREDENTIAL" => Ok(SyntaxType::CREDENTIAL),
|
"CREDENTIAL" => Ok(SyntaxType::CREDENTIAL),
|
||||||
|
"RADIUS_UTF8STRING" => Ok(SyntaxType::RADIUS_UTF8STRING),
|
||||||
|
"SSHKEY" => Ok(SyntaxType::SSHKEY),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,6 +124,8 @@ impl TryFrom<usize> for SyntaxType {
|
||||||
6 => Ok(SyntaxType::REFERENCE_UUID),
|
6 => Ok(SyntaxType::REFERENCE_UUID),
|
||||||
7 => Ok(SyntaxType::JSON_FILTER),
|
7 => Ok(SyntaxType::JSON_FILTER),
|
||||||
8 => Ok(SyntaxType::CREDENTIAL),
|
8 => Ok(SyntaxType::CREDENTIAL),
|
||||||
|
9 => Ok(SyntaxType::RADIUS_UTF8STRING),
|
||||||
|
10 => Ok(SyntaxType::SSHKEY),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,6 +143,8 @@ impl SyntaxType {
|
||||||
SyntaxType::REFERENCE_UUID => "REFERENCE_UUID",
|
SyntaxType::REFERENCE_UUID => "REFERENCE_UUID",
|
||||||
SyntaxType::JSON_FILTER => "JSON_FILTER",
|
SyntaxType::JSON_FILTER => "JSON_FILTER",
|
||||||
SyntaxType::CREDENTIAL => "CREDENTIAL",
|
SyntaxType::CREDENTIAL => "CREDENTIAL",
|
||||||
|
SyntaxType::RADIUS_UTF8STRING => "RADIUS_UTF8STRING",
|
||||||
|
SyntaxType::SSHKEY => "SSHKEY",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +159,8 @@ impl SyntaxType {
|
||||||
SyntaxType::REFERENCE_UUID => 6,
|
SyntaxType::REFERENCE_UUID => 6,
|
||||||
SyntaxType::JSON_FILTER => 7,
|
SyntaxType::JSON_FILTER => 7,
|
||||||
SyntaxType::CREDENTIAL => 8,
|
SyntaxType::CREDENTIAL => 8,
|
||||||
|
SyntaxType::RADIUS_UTF8STRING => 9,
|
||||||
|
SyntaxType::SSHKEY => 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,8 +168,8 @@ impl SyntaxType {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum DataValue {
|
pub enum DataValue {
|
||||||
Cred(Credential),
|
Cred(Credential),
|
||||||
// SshKey(String),
|
SshKey(String),
|
||||||
// RadiusCred(String),
|
RadiusCred(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Deserialize, Serialize)]
|
||||||
|
@ -176,8 +186,8 @@ pub enum PartialValue {
|
||||||
JsonFilt(ProtoFilter),
|
JsonFilt(ProtoFilter),
|
||||||
// Tag, matches to a DataValue.
|
// Tag, matches to a DataValue.
|
||||||
Cred(String),
|
Cred(String),
|
||||||
// SshKey(String),
|
SshKey(String),
|
||||||
// RadiusCred(String),
|
RadiusCred,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialValue {
|
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> {
|
pub fn to_str(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
PartialValue::Utf8(s) => Some(s.as_str()),
|
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")
|
serde_json::to_string(s).expect("A json filter value was corrupted during run-time")
|
||||||
}
|
}
|
||||||
PartialValue::Cred(tag) => tag.to_string(),
|
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 {
|
PartialValue::Cred(_) => match &self.data {
|
||||||
Some(dv) => match dv {
|
Some(dv) => match dv {
|
||||||
DataValue::Cred(c) => Some(&c),
|
DataValue::Cred(c) => Some(&c),
|
||||||
|
_ => None,
|
||||||
},
|
},
|
||||||
None => 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 {
|
pub fn contains(&self, s: &PartialValue) -> bool {
|
||||||
self.pv.contains(s)
|
self.pv.contains(s)
|
||||||
}
|
}
|
||||||
|
@ -756,6 +826,14 @@ impl Value {
|
||||||
data: Some(DataValue::Cred(Credential::try_from(dvc.d)?)),
|
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) => {
|
PartialValue::Cred(tag) => {
|
||||||
// Get the credential out and make sure it matches the type we expect.
|
// Get the credential out and make sure it matches the type we expect.
|
||||||
let c = match &self.data {
|
let c = match &self.data {
|
||||||
Some(v) => {
|
Some(v) => match &v {
|
||||||
match &v {
|
DataValue::Cred(c) => c,
|
||||||
DataValue::Cred(c) => c,
|
_ => panic!(),
|
||||||
// _ => panic!(),
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
None => panic!(),
|
None => panic!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -791,6 +867,29 @@ impl Value {
|
||||||
d: c.to_db_valuev1(),
|
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 the proto side. The credentials private data is stored seperately.
|
||||||
tag.to_string()
|
tag.to_string()
|
||||||
}
|
}
|
||||||
|
PartialValue::SshKey(tag) => tag.to_string(),
|
||||||
|
PartialValue::RadiusCred => "radius".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -885,12 +986,24 @@ impl Value {
|
||||||
// data.
|
// data.
|
||||||
match &self.pv {
|
match &self.pv {
|
||||||
PartialValue::Cred(_) => match &self.data {
|
PartialValue::Cred(_) => match &self.data {
|
||||||
Some(v) => {
|
Some(v) => match &v {
|
||||||
match &v {
|
DataValue::Cred(_) => true,
|
||||||
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,
|
None => false,
|
||||||
},
|
},
|
||||||
_ => true,
|
_ => true,
|
||||||
|
@ -909,6 +1022,8 @@ impl Value {
|
||||||
PartialValue::JsonFilt(s) => vec![serde_json::to_string(s)
|
PartialValue::JsonFilt(s) => vec![serde_json::to_string(s)
|
||||||
.expect("A json filter value was corrupted during run-time")],
|
.expect("A json filter value was corrupted during run-time")],
|
||||||
PartialValue::Cred(tag) => vec![tag.to_string()],
|
PartialValue::Cred(tag) => vec![tag.to_string()],
|
||||||
|
PartialValue::SshKey(tag) => vec![tag.to_string()],
|
||||||
|
PartialValue::RadiusCred => vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue