From c00634188442deabff8c641965502f074494a840 Mon Sep 17 00:00:00 2001 From: Firstyear Date: Thu, 31 Oct 2019 10:48:15 +1000 Subject: [PATCH] 17 radius (#123) Majority of radius integration and tooling complete, including docker files. --- .dockerignore | 1 + .gitignore | 1 + README.md | 6 +- kanidm_client/src/lib.rs | 43 +- kanidm_client/tests/proto_v1_test.rs | 46 ++ kanidm_proto/src/v1.rs | 21 + kanidm_rlm_python/Dockerfile | 36 + kanidm_rlm_python/config.ini | 25 + kanidm_rlm_python/default | 964 +++++++++++++++++++++++++ kanidm_rlm_python/entrypoint.py | 94 +++ kanidm_rlm_python/inner-tunnel | 440 +++++++++++ kanidm_rlm_python/kanidmradius.py | 138 ++++ kanidm_rlm_python/mod-python | 65 ++ kanidm_rlm_python/test_data/config.ini | 29 + kanidm_tools/src/main.rs | 46 ++ Dockerfile => kanidmd/Dockerfile | 2 +- kanidmd/src/lib/access.rs | 107 ++- kanidmd/src/lib/actors/v1_read.rs | 131 +++- kanidmd/src/lib/actors/v1_write.rs | 121 +++- kanidmd/src/lib/be/dbvalue.rs | 8 + kanidmd/src/lib/be/mod.rs | 27 + kanidmd/src/lib/constants.rs | 44 +- kanidmd/src/lib/core.rs | 207 +++++- kanidmd/src/lib/entry.rs | 287 ++++---- kanidmd/src/lib/event.rs | 76 +- kanidmd/src/lib/idm/account.rs | 110 ++- kanidmd/src/lib/idm/event.rs | 66 +- kanidmd/src/lib/idm/group.rs | 106 ++- kanidmd/src/lib/idm/macros.rs | 2 +- kanidmd/src/lib/idm/mod.rs | 1 + kanidmd/src/lib/idm/radius.rs | 78 ++ kanidmd/src/lib/idm/server.rs | 150 +++- kanidmd/src/lib/lib.rs | 2 +- kanidmd/src/lib/modify.rs | 4 + kanidmd/src/lib/schema.rs | 40 + kanidmd/src/lib/server.rs | 57 ++ kanidmd/src/lib/utils.rs | 11 + kanidmd/src/lib/value.rs | 149 +++- 38 files changed, 3467 insertions(+), 274 deletions(-) create mode 100644 kanidm_rlm_python/Dockerfile create mode 100644 kanidm_rlm_python/config.ini create mode 100644 kanidm_rlm_python/default create mode 100644 kanidm_rlm_python/entrypoint.py create mode 100644 kanidm_rlm_python/inner-tunnel create mode 100644 kanidm_rlm_python/kanidmradius.py create mode 100644 kanidm_rlm_python/mod-python create mode 100644 kanidm_rlm_python/test_data/config.ini rename Dockerfile => kanidmd/Dockerfile (81%) create mode 100644 kanidmd/src/lib/idm/radius.rs diff --git a/.dockerignore b/.dockerignore index a7a514da5..a2350ff47 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,5 @@ target .git .gitignore test.db +vendor diff --git a/.gitignore b/.gitignore index 61c358abc..060c0a3fa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ **/*.rs.bk test.db /vendor +kanidm_rlm_python/test_data/certs/ diff --git a/README.md b/README.md index 8d8e7db39..8c98f337e 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,12 @@ In a new terminal, you can now build and run the client tools with: cd kanidm_tools cargo run -- --help - cargo run -- whoami -H https://localhost:8080 -D anonymous -C ../insecure/ca.pem - cargo run -- whoami -H https://localhost:8080 -D admin -C ../insecure/ca.pem + cargo run -- self whoami -H https://localhost:8080 -D anonymous -C ../insecure/ca.pem + cargo run -- self whoami -H https://localhost:8080 -D admin -C ../insecure/ca.pem For more see [getting started] -[getting started]: https://github.com/Firstyear/kanidm/blob/master/GETTING_STARTED.html +[getting started]: https://github.com/Firstyear/kanidm/blob/master/GETTING_STARTED.md ## Development and Testing diff --git a/kanidm_client/src/lib.rs b/kanidm_client/src/lib.rs index d7df97559..8057d1e73 100644 --- a/kanidm_client/src/lib.rs +++ b/kanidm_client/src/lib.rs @@ -12,8 +12,9 @@ use std::io::Read; use kanidm_proto::v1::{ AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest, - Entry, Filter, ModifyList, ModifyRequest, OperationError, OperationResponse, SearchRequest, - SearchResponse, SetAuthCredential, SingleStringRequest, UserAuthToken, WhoamiResponse, + Entry, Filter, ModifyList, ModifyRequest, OperationError, OperationResponse, RadiusAuthToken, + SearchRequest, SearchResponse, SetAuthCredential, SingleStringRequest, UserAuthToken, + WhoamiResponse, }; use serde_json; @@ -155,6 +156,22 @@ impl KanidmClient { Ok(r) } + fn perform_delete_request(&self, dest: &str) -> Result<(), ClientError> { + let dest = format!("{}{}", self.addr, dest); + let mut response = self + .client + .delete(dest.as_str()) + .send() + .map_err(|e| ClientError::Transport(e))?; + + match response.status() { + reqwest::StatusCode::OK => {} + unexpect => return Err(ClientError::Http(unexpect, response.json().ok())), + } + + Ok(()) + } + // whoami // Can't use generic get due to possible un-auth case. pub fn whoami(&self) -> Result, ClientError> { @@ -324,6 +341,28 @@ impl KanidmClient { }) } + pub fn idm_account_radius_credential_get( + &self, + id: &str, + ) -> Result, ClientError> { + self.perform_get_request(format!("/v1/account/{}/_radius", id).as_str()) + } + + pub fn idm_account_radius_credential_regenerate( + &self, + id: &str, + ) -> Result { + 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 { + self.perform_get_request(format!("/v1/account/{}/_radius/_token", id).as_str()) + } + // ==== schema pub fn idm_schema_list(&self) -> Result, ClientError> { self.perform_get_request("/v1/schema") diff --git a/kanidm_client/tests/proto_v1_test.rs b/kanidm_client/tests/proto_v1_test.rs index acdff7a09..52e4318e9 100644 --- a/kanidm_client/tests/proto_v1_test.rs +++ b/kanidm_client/tests/proto_v1_test.rs @@ -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. diff --git a/kanidm_proto/src/v1.rs b/kanidm_proto/src/v1.rs index a7f479b67..445dc5432 100644 --- a/kanidm_proto/src/v1.rs +++ b/kanidm_proto/src/v1.rs @@ -139,6 +139,27 @@ impl fmt::Display for UserAuthToken { // UAT will need a downcast to Entry, which adds in the claims to the entry // for the purpose of filtering. +// This is similar to uat, but omits claims (they have no role in radius), and adds +// the radius secret field. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RadiusAuthToken { + pub name: String, + pub displayname: String, + pub uuid: String, + pub secret: String, + pub groups: Vec, +} + +impl fmt::Display for RadiusAuthToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "name: {}", self.name)?; + writeln!(f, "display: {}", self.displayname)?; + writeln!(f, "uuid: {}", self.uuid)?; + writeln!(f, "secret: {}", self.secret)?; + writeln!(f, "groups: {:?}", self.groups) + } +} + /* ===== low level proto types ===== */ // ProtoEntry vs Entry diff --git a/kanidm_rlm_python/Dockerfile b/kanidm_rlm_python/Dockerfile new file mode 100644 index 000000000..bc4ec7ae3 --- /dev/null +++ b/kanidm_rlm_python/Dockerfile @@ -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" ] diff --git a/kanidm_rlm_python/config.ini b/kanidm_rlm_python/config.ini new file mode 100644 index 000000000..7a7c71e10 --- /dev/null +++ b/kanidm_rlm_python/config.ini @@ -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 = + diff --git a/kanidm_rlm_python/default b/kanidm_rlm_python/default new file mode 100644 index 000000000..293d9b847 --- /dev/null +++ b/kanidm_rlm_python/default @@ -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 +# } +} +} diff --git a/kanidm_rlm_python/entrypoint.py b/kanidm_rlm_python/entrypoint.py new file mode 100644 index 000000000..fd3f8ec1e --- /dev/null +++ b/kanidm_rlm_python/entrypoint.py @@ -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() + diff --git a/kanidm_rlm_python/inner-tunnel b/kanidm_rlm_python/inner-tunnel new file mode 100644 index 000000000..159cf581b --- /dev/null +++ b/kanidm_rlm_python/inner-tunnel @@ -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 diff --git a/kanidm_rlm_python/kanidmradius.py b/kanidm_rlm_python/kanidmradius.py new file mode 100644 index 000000000..583727fcd --- /dev/null +++ b/kanidm_rlm_python/kanidmradius.py @@ -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 + + diff --git a/kanidm_rlm_python/mod-python b/kanidm_rlm_python/mod-python new file mode 100644 index 000000000..5f7f56fe4 --- /dev/null +++ b/kanidm_rlm_python/mod-python @@ -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 +} diff --git a/kanidm_rlm_python/test_data/config.ini b/kanidm_rlm_python/test_data/config.ini new file mode 100644 index 000000000..f7d21694f --- /dev/null +++ b/kanidm_rlm_python/test_data/config.ini @@ -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 + diff --git a/kanidm_tools/src/main.rs b/kanidm_tools/src/main.rs index 58ee8928a..b7cf7fdd2 100644 --- a/kanidm_tools/src/main.rs +++ b/kanidm_tools/src/main.rs @@ -102,6 +102,14 @@ struct AccountCredentialSet { copt: CommonOpt, } +#[derive(Debug, StructOpt)] +struct AccountRadiusOpt { + #[structopt(flatten)] + aopts: AccountCommonOpt, + #[structopt(flatten)] + copt: CommonOpt, +} + #[derive(Debug, StructOpt)] enum AccountCredential { #[structopt(name = "set_password")] @@ -110,10 +118,22 @@ enum AccountCredential { GeneratePassword(AccountCredentialSet), } +#[derive(Debug, StructOpt)] +enum AccountRadius { + #[structopt(name = "show_secret")] + Show(AccountRadiusOpt), + #[structopt(name = "generate_secret")] + Generate(AccountRadiusOpt), + #[structopt(name = "delete_secret")] + Delete(AccountRadiusOpt), +} + #[derive(Debug, StructOpt)] enum AccountOpt { #[structopt(name = "credential")] Credential(AccountCredential), + #[structopt(name = "radius")] + Radius(AccountRadius), } #[derive(Debug, StructOpt)] @@ -274,6 +294,32 @@ fn main() { ); } }, // end AccountOpt::Credential + AccountOpt::Radius(aropt) => match aropt { + AccountRadius::Show(aopt) => { + let client = aopt.copt.to_client(); + + let rcred = client + .idm_account_radius_credential_get(aopt.aopts.account_id.as_str()) + .unwrap(); + + match rcred { + Some(s) => println!("Radius secret: {}", s), + None => println!("NO Radius secret"), + } + } + AccountRadius::Generate(aopt) => { + let client = aopt.copt.to_client(); + client + .idm_account_radius_credential_regenerate(aopt.aopts.account_id.as_str()) + .unwrap(); + } + AccountRadius::Delete(aopt) => { + let client = aopt.copt.to_client(); + client + .idm_account_radius_credential_delete(aopt.aopts.account_id.as_str()) + .unwrap(); + } + }, // end AccountOpt::Radius }, } } diff --git a/Dockerfile b/kanidmd/Dockerfile similarity index 81% rename from Dockerfile rename to kanidmd/Dockerfile index 976df3501..bdd617eb2 100644 --- a/Dockerfile +++ b/kanidmd/Dockerfile @@ -18,5 +18,5 @@ RUN cd /etc && \ VOLUME /data ENV RUST_BACKTRACE 1 -CMD ["/home/kanidm/target/release/kanidmd", "server", "-D", "/data/kanidm.db"] +CMD ["/home/kanidm/target/release/kanidmd", "server", "-D", "/data/kanidm.db", "-C", "/data/ca.pem", "-c", "/data/cert.pem", "-k", "/data/key.pem", "--bindaddr", "0.0.0.0:8080"] diff --git a/kanidmd/src/lib/access.rs b/kanidmd/src/lib/access.rs index c4159ab8c..1675512fd 100644 --- a/kanidmd/src/lib/access.rs +++ b/kanidmd/src/lib/access.rs @@ -46,7 +46,7 @@ lazy_static! { pub struct AccessControlSearch { acp: AccessControlProfile, // TODO: Should this change to Value? May help to reduce transformations during processing. - attrs: Vec, + attrs: BTreeSet, } impl AccessControlSearch { @@ -65,7 +65,7 @@ impl AccessControlSearch { let attrs = try_audit!( audit, value - .get_ava_string("acp_search_attr") + .get_ava_set_string("acp_search_attr") .ok_or(OperationError::InvalidACPState( "Missing acp_search_attr".to_string() )) @@ -521,9 +521,20 @@ pub trait AccessControlsTransaction { // interface is beyond me .... let rec_entry: &Entry = match &se.event.origin { EventOrigin::Internal => { - audit_log!(audit, "IMPOSSIBLE STATE: Internal search in external interface?! Returning empty for safety."); - // No need to check ACS - return Ok(Vec::new()); + if cfg!(test) { + audit_log!(audit, "TEST: Internal search in external interface - allowing due to cfg test ..."); + // In tests we just push everything back. + return Ok(entries + .into_iter() + .map(|e| unsafe { e.to_reduced() }) + .collect()); + } else { + // In production we can't risk leaking data here, so we return + // empty sets. + audit_log!(audit, "IMPOSSIBLE STATE: Internal search in external interface?! Returning empty for safety."); + // No need to check ACS + return Ok(Vec::new()); + } } EventOrigin::User(e) => &e, }; @@ -539,8 +550,26 @@ pub trait AccessControlsTransaction { let f_val = acs.acp.receiver.clone(); match f_val.resolve(&se.event, None) { Ok(f_res) => { + // Is our user covered by this acs? if rec_entry.entry_match_no_index(&f_res) { - 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 { None } @@ -561,10 +590,11 @@ pub trait AccessControlsTransaction { audit_log!(audit, "Related acs -> {:?}", racp.acp.name); }); - // Get the set of attributes requested by the caller - // TODO #69: This currently - // is ALL ATTRIBUTES, so we actually work here to just remove things we - // CAN'T see instead. + // Build a reference set from the req_attrs + let req_attrs: Option> = se + .attrs + .as_ref() + .map(|vs| vs.iter().map(|s| s.as_str()).collect()); // For each entry let allowed_entries: Vec> = entries @@ -611,12 +641,20 @@ pub trait AccessControlsTransaction { }) .flatten() .collect(); + // Remove all others that are present on the entry. audit_log!(audit, "-- for entry --> {:?}", e.get_uuid()); + audit_log!(audit, "requested attributes --> {:?}", req_attrs); audit_log!(audit, "allowed attributes --> {:?}", allowed_attrs); + // Remove anything that wasn't requested. + let f_allowed_attrs: BTreeSet<&str> = match &req_attrs { + Some(v) => allowed_attrs.intersection(&v).map(|s| *s).collect(), + None => allowed_attrs, + }; + // Now purge the attrs that are NOT in this. - e.reduce_attributes(allowed_attrs) + e.reduce_attributes(f_allowed_attrs) }) .collect(); Ok(allowed_entries) @@ -1789,8 +1827,10 @@ mod tests { .expect("operation failed"); // Help the type checker for the expect set. - let expect_set: Vec> = - $expect.into_iter().map(|e| e.to_reduced()).collect(); + let expect_set: Vec> = $expect + .into_iter() + .map(|e| unsafe { e.to_reduced() }) + .collect(); println!("expect --> {:?}", expect_set); println!("result --> {:?}", reduced); @@ -1846,6 +1886,47 @@ mod tests { test_acp_search_reduce!(&se_anon, vec![acp], r_set, ex_anon); } + #[test] + fn test_access_enforce_search_attrs_req() { + // Test that attributes are correctly limited by the request. + // In this case, we test that a user can only see "name" despite the + // class and uuid being present. + let e1: Entry = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); + let ev1 = unsafe { e1.to_valid_committed() }; + let r_set = vec![ev1.clone()]; + + let ex1: Entry = + Entry::unsafe_from_entry_str(JSON_TESTPERSON1_REDUCED); + let exv1 = unsafe { ex1.to_valid_committed() }; + let ex_anon = vec![exv1.clone()]; + + let mut se_anon = unsafe { + SearchEvent::new_impersonate_entry_ser( + JSON_ANONYMOUS_V1, + filter_all!(f_eq("name", PartialValue::new_iutf8s("testperson1"))), + ) + }; + // the requested attrs here. + se_anon.attrs = Some(btreeset!["name".to_string()]); + + let acp = unsafe { + AccessControlSearch::from_raw( + "test_acp", + "d38640c4-0254-49f9-99b7-8ba7d0233f3d", + // apply to anonymous only + filter_valid!(f_eq("name", PartialValue::new_iutf8s("anonymous"))), + // Allow anonymous to read only testperson1 + filter_valid!(f_eq("name", PartialValue::new_iutf8s("testperson1"))), + // In that read, admin may only view the "name" attribute, or query on + // the name attribute. Any other query (should be) rejected. + "name uuid", + ) + }; + + // Finally test it! + test_acp_search_reduce!(&se_anon, vec![acp], r_set, ex_anon); + } + macro_rules! test_acp_modify { ( $me:expr, diff --git a/kanidmd/src/lib/actors/v1_read.rs b/kanidmd/src/lib/actors/v1_read.rs index b6166963f..504be85f6 100644 --- a/kanidmd/src/lib/actors/v1_read.rs +++ b/kanidmd/src/lib/actors/v1_read.rs @@ -4,7 +4,8 @@ use crate::audit::AuditScope; use crate::async_log::EventLog; use crate::event::{AuthEvent, SearchEvent, SearchResult, WhoamiResult}; -use kanidm_proto::v1::OperationError; +use crate::idm::event::RadiusAuthTokenEvent; +use kanidm_proto::v1::{OperationError, RadiusAuthToken}; use crate::filter::{Filter, FilterInvalid}; use crate::idm::server::IdmServer; @@ -77,21 +78,31 @@ impl Message for SearchMessage { pub struct InternalSearchMessage { pub uat: Option, pub filter: Filter, -} - -impl InternalSearchMessage { - pub fn new(uat: Option, filter: Filter) -> Self { - InternalSearchMessage { - uat: uat, - filter: filter, - } - } + pub attrs: Option>, } impl Message for InternalSearchMessage { type Result = Result, OperationError>; } +pub struct InternalRadiusReadMessage { + pub uat: Option, + pub uuid_or_name: String, +} + +impl Message for InternalRadiusReadMessage { + type Result = Result, OperationError>; +} + +pub struct InternalRadiusTokenReadMessage { + pub uat: Option, + pub uuid_or_name: String, +} + +impl Message for InternalRadiusTokenReadMessage { + type Result = Result; +} + // =========================================================== pub struct QueryServerReadV1 { @@ -297,3 +308,103 @@ impl Handler for QueryServerReadV1 { res } } + +impl Handler for QueryServerReadV1 { + type Result = Result, 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 for QueryServerReadV1 { + type Result = Result; + + 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 + } +} diff --git a/kanidmd/src/lib/actors/v1_write.rs b/kanidmd/src/lib/actors/v1_write.rs index 632693ca8..802730a7f 100644 --- a/kanidmd/src/lib/actors/v1_write.rs +++ b/kanidmd/src/lib/actors/v1_write.rs @@ -5,7 +5,7 @@ use crate::async_log::EventLog; use crate::event::{ CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent, }; -use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent}; +use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent, RegenerateRadiusSecretEvent}; use kanidm_proto::v1::OperationError; use crate::idm::server::IdmServer; @@ -109,6 +109,36 @@ impl Message for InternalCredentialSetMessage { type Result = Result, OperationError>; } +pub struct InternalRegenerateRadiusMessage { + pub uat: Option, + pub uuid_or_name: String, +} + +impl InternalRegenerateRadiusMessage { + pub fn new(uat: Option, uuid_or_name: String) -> Self { + InternalRegenerateRadiusMessage { + uat: uat, + uuid_or_name: uuid_or_name, + } + } +} + +impl Message for InternalRegenerateRadiusMessage { + type Result = Result; +} + +/// 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, + pub uuid_or_name: String, + pub attr: String, +} + +impl Message for PurgeAttributeMessage { + type Result = Result<(), OperationError>; +} + pub struct QueryServerWriteV1 { log: actix::Addr, qs: QueryServer, @@ -331,6 +361,95 @@ impl Handler for QueryServerWriteV1 { } } +impl Handler for QueryServerWriteV1 { + type Result = Result; + + 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 for QueryServerWriteV1 { + type Result = Result<(), OperationError>; + + fn handle(&mut self, msg: PurgeAttributeMessage, _: &mut Self::Context) -> Self::Result { + let mut audit = AuditScope::new("modify"); + let res = audit_segment!(&mut audit, || { + let mut qs_write = self.qs.write(); + let target_uuid = match Uuid::parse_str(msg.uuid_or_name.as_str()) { + Ok(u) => u, + Err(_) => qs_write + .name_to_uuid(&mut audit, msg.uuid_or_name.as_str()) + .map_err(|e| { + audit_log!(&mut audit, "Error resolving id to target"); + e + })?, + }; + + let mdf = match ModifyEvent::from_target_uuid_attr_purge( + &mut audit, + msg.uat, + target_uuid, + msg.attr, + &qs_write, + ) { + Ok(m) => m, + Err(e) => { + audit_log!(audit, "Failed to begin modify: {:?}", e); + return Err(e); + } + }; + + audit_log!(audit, "Begin modify event {:?}", mdf); + + qs_write + .modify(&mut audit, &mdf) + .and_then(|_| qs_write.commit(&mut audit).map(|_| ())) + }); + self.log.do_send(audit); + res + } +} + // These below are internal only types. impl Handler for QueryServerWriteV1 { diff --git a/kanidmd/src/lib/be/dbvalue.rs b/kanidmd/src/lib/be/dbvalue.rs index 5d511ca36..586e0868c 100644 --- a/kanidmd/src/lib/be/dbvalue.rs +++ b/kanidmd/src/lib/be/dbvalue.rs @@ -18,6 +18,12 @@ pub struct DbValueCredV1 { pub d: DbCredV1, } +#[derive(Serialize, Deserialize, Debug)] +pub struct DbValueTaggedStringV1 { + pub t: String, + pub d: String, +} + #[derive(Serialize, Deserialize, Debug)] pub enum DbValueV1 { U8(String), @@ -29,4 +35,6 @@ pub enum DbValueV1 { RF(Uuid), JF(String), CR(DbValueCredV1), + RU(String), + SK(DbValueTaggedStringV1), } diff --git a/kanidmd/src/lib/be/mod.rs b/kanidmd/src/lib/be/mod.rs index 91c57bcf1..5bda6bdbb 100644 --- a/kanidmd/src/lib/be/mod.rs +++ b/kanidmd/src/lib/be/mod.rs @@ -129,6 +129,8 @@ pub trait BackendTransaction { } } FilterResolved::Or(l) => { + // Importantly if this has no inner elements, this returns + // an empty list. let mut result = IDLBitRange::new(); let mut partial = false; // For each filter in l @@ -1896,6 +1898,31 @@ mod tests { panic!(""); } } + + // empty or + let f_e_or = unsafe { filter_resolved!(f_or!([])) }; + + let r = be.filter2idl(audit, f_e_or.to_inner(), 0).unwrap(); + match r { + IDL::Indexed(idl) => { + assert!(idl == IDLBitRange::from_iter(vec![])); + } + _ => { + panic!(""); + } + } + + let f_e_and = unsafe { filter_resolved!(f_and!([])) }; + + let r = be.filter2idl(audit, f_e_and.to_inner(), 0).unwrap(); + match r { + IDL::Indexed(idl) => { + assert!(idl == IDLBitRange::from_iter(vec![])); + } + _ => { + panic!(""); + } + } }) } diff --git a/kanidmd/src/lib/constants.rs b/kanidmd/src/lib/constants.rs index 7cb00f8d8..e1a9863b0 100644 --- a/kanidmd/src/lib/constants.rs +++ b/kanidmd/src/lib/constants.rs @@ -356,6 +356,7 @@ pub static JSON_IDM_SELF_ACP_READ_V1: &'static str = r#"{ "class", "memberof", "member", + "radius_secret", "uuid" ] } @@ -367,7 +368,7 @@ pub static JSON_IDM_SELF_ACP_WRITE_V1: &'static str = r#"{ "class": ["object", "access_control_profile", "access_control_modify"], "name": ["idm_self_acp_write"], "uuid": ["00000000-0000-0000-0000-ffffff000021"], - "description": ["Builtin IDM Control for self write - required for people to update their own identities in line with best practices."], + "description": ["Builtin IDM Control for self write - required for people to update their own identities and credentials in line with best practices."], "acp_enable": ["true"], "acp_receiver": [ "{\"And\": [\"Self\", {\"AndNot\": {\"Or\": [{\"Eq\": [\"class\", \"tombstone\"]}, {\"Eq\": [\"class\", \"recycled\"]}, {\"Eq\": [\"uuid\", \"00000000-0000-0000-0000-ffffffffffff\"]}]}}]}" @@ -376,10 +377,10 @@ pub static JSON_IDM_SELF_ACP_WRITE_V1: &'static str = r#"{ "\"Self\"" ], "acp_modify_removedattr": [ - "name", "displayname", "legalname" + "name", "displayname", "legalname", "radius_secret", "primary_credential" ], "acp_modify_presentattr": [ - "name", "displayname", "legalname" + "name", "displayname", "legalname", "radius_secret", "primary_credential" ] } }"#; @@ -650,7 +651,6 @@ pub static JSON_IDM_ACP_PERSON_ACCOUNT_CREATE_V1: &'static str = r#"{ pub static _UUID_IDM_ACP_RADIUS_SERVERS_V1: &'static str = "00000000-0000-0000-0000-ffffff000014"; // The targetscope of this could change later to a "radius access" group or similar so we can add/remove // users from having radius access easier. -// TODO #17: Add the radius credential type that we need to read here. pub static JSON_IDM_ACP_RADIUS_SERVERS_V1: &'static str = r#"{ "attrs": { "class": [ @@ -669,7 +669,7 @@ pub static JSON_IDM_ACP_RADIUS_SERVERS_V1: &'static str = r#"{ "{\"And\": [{\"Pres\": \"class\"}, {\"AndNot\": {\"Or\": [{\"Eq\": [\"class\", \"tombstone\"]}, {\"Eq\": [\"class\", \"recycled\"]}]}}]}" ], "acp_search_attr": [ - "name", "uuid" + "name", "uuid", "radius_secret" ] } }"#; @@ -1178,7 +1178,7 @@ pub static JSON_SCHEMA_ATTR_SSH_PUBLICKEY: &'static str = r#" "ssh_publickey" ], "syntax": [ - "UTF8STRING" + "SSHKEY" ], "uuid": [ "00000000-0000-0000-0000-ffff00000042" @@ -1253,6 +1253,35 @@ pub static JSON_SCHEMA_ATTR_LEGALNAME: &'static str = r#"{ ] } }"#; +pub static UUID_SCHEMA_ATTR_RADIUS_SECRET: &'static str = "00000000-0000-0000-0000-ffff00000051"; +pub static JSON_SCHEMA_ATTR_RADIUS_SECRET: &'static str = r#"{ + "attrs": { + "class": [ + "object", + "system", + "attributetype" + ], + "description": [ + "The accounts generated radius secret for device network authentication" + ], + "index": [], + "unique": [ + "false" + ], + "multivalue": [ + "false" + ], + "attributename": [ + "radius_secret" + ], + "syntax": [ + "RADIUS_UTF8STRING" + ], + "uuid": [ + "00000000-0000-0000-0000-ffff00000051" + ] + } +}"#; pub static UUID_SCHEMA_CLASS_PERSON: &'static str = "00000000-0000-0000-0000-ffff00000044"; pub static JSON_SCHEMA_CLASS_PERSON: &'static str = r#" @@ -1336,7 +1365,8 @@ pub static JSON_SCHEMA_CLASS_ACCOUNT: &'static str = r#" ], "systemmay": [ "primary_credential", - "ssh_publickey" + "ssh_publickey", + "radius_secret" ], "systemmust": [ "displayname", diff --git a/kanidmd/src/lib/core.rs b/kanidmd/src/lib/core.rs index f9953ef58..d8fd40f22 100644 --- a/kanidmd/src/lib/core.rs +++ b/kanidmd/src/lib/core.rs @@ -15,11 +15,14 @@ use crate::config::Configuration; // SearchResult use crate::actors::v1_read::QueryServerReadV1; -use crate::actors::v1_read::{AuthMessage, InternalSearchMessage, SearchMessage, WhoamiMessage}; +use crate::actors::v1_read::{ + AuthMessage, InternalRadiusReadMessage, InternalRadiusTokenReadMessage, InternalSearchMessage, + SearchMessage, WhoamiMessage, +}; use crate::actors::v1_write::QueryServerWriteV1; use crate::actors::v1_write::{ CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage, InternalCredentialSetMessage, - ModifyMessage, + InternalRegenerateRadiusMessage, ModifyMessage, PurgeAttributeMessage, }; use crate::async_log; use crate::audit::AuditScope; @@ -187,12 +190,17 @@ fn json_rest_event_get( req: HttpRequest, state: State, filter: Filter, + attrs: Option>, ) -> impl Future { let uat = get_current_user(&req); // TODO: I think we'll need to change this to take an internal filter // type that we send to the qs. - let obj = InternalSearchMessage::new(uat, filter); + let obj = InternalSearchMessage { + uat: uat, + filter: filter, + attrs: attrs, + }; let res = state.qe_r.send(obj).from_err().and_then(|res| match res { Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)), @@ -207,12 +215,17 @@ fn json_rest_event_get_id( req: HttpRequest, state: State, filter: Filter, + attrs: Option>, ) -> impl Future { let uat = get_current_user(&req); let filter = Filter::join_parts_and(filter, filter_all!(f_id(path.as_str()))); - let obj = InternalSearchMessage::new(uat, filter); + let obj = InternalSearchMessage { + uat: uat, + filter: filter, + attrs: attrs, + }; let res = state.qe_r.send(obj).from_err().and_then(|res| match res { Ok(mut event_result) => { @@ -225,6 +238,71 @@ fn json_rest_event_get_id( Box::new(res) } +fn json_rest_event_get_id_attr( + path: Path, + req: HttpRequest, + state: State, + filter: Filter, + attr: String, +) -> impl Future { + 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, + req: HttpRequest, + state: State, + attr: String, +) -> impl Future { + let uat = get_current_user(&req); + let id = path.into_inner(); + + let obj = PurgeAttributeMessage { + uat: uat, + uuid_or_name: id, + attr: attr, + }; + + let res = state.qe_w.send(obj).from_err().and_then(|res| match res { + Ok(event_result) => { + // Only send back the first result, or None + Ok(HttpResponse::Ok().json(event_result)) + } + Err(e) => Ok(operation_error_to_response(e)), + }); + + Box::new(res) +} + fn json_rest_event_credential_put( id: String, cred_id: Option, @@ -299,14 +377,14 @@ fn schema_get( f_eq("class", PartialValue::new_class("attributetype")), f_eq("class", PartialValue::new_class("classtype")) ])); - json_rest_event_get(req, state, filter) + json_rest_event_get(req, state, filter, None) } fn schema_attributetype_get( (req, state): (HttpRequest, State), ) -> impl Future { let filter = filter_all!(f_eq("class", PartialValue::new_class("attributetype"))); - json_rest_event_get(req, state, filter) + json_rest_event_get(req, state, filter, None) } fn schema_attributetype_get_id( @@ -320,7 +398,11 @@ fn schema_attributetype_get_id( f_eq("attributename", PartialValue::new_iutf8s(path.as_str())) ])); - let obj = InternalSearchMessage::new(uat, filter); + let obj = InternalSearchMessage { + uat: uat, + filter: filter, + attrs: None, + }; let res = state.qe_r.send(obj).from_err().and_then(|res| match res { Ok(mut event_result) => { @@ -337,7 +419,7 @@ fn schema_classtype_get( (req, state): (HttpRequest, State), ) -> impl Future { let filter = filter_all!(f_eq("class", PartialValue::new_class("classtype"))); - json_rest_event_get(req, state, filter) + json_rest_event_get(req, state, filter, None) } fn schema_classtype_get_id( @@ -351,7 +433,11 @@ fn schema_classtype_get_id( f_eq("classname", PartialValue::new_iutf8s(path.as_str())) ])); - let obj = InternalSearchMessage::new(uat, filter); + let obj = InternalSearchMessage { + uat: uat, + filter: filter, + attrs: None, + }; let res = state.qe_r.send(obj).from_err().and_then(|res| match res { Ok(mut event_result) => { @@ -368,14 +454,14 @@ fn account_get( (req, state): (HttpRequest, State), ) -> impl Future { let filter = filter_all!(f_eq("class", PartialValue::new_class("account"))); - json_rest_event_get(req, state, filter) + json_rest_event_get(req, state, filter, None) } fn account_get_id( (path, req, state): (Path, HttpRequest, State), ) -> impl Future { let filter = filter_all!(f_eq("class", PartialValue::new_class("account"))); - json_rest_event_get_id(path, req, state, filter) + json_rest_event_get_id(path, req, state, filter, None) } fn account_put_id_credential_primary( @@ -385,18 +471,89 @@ fn account_put_id_credential_primary( json_rest_event_credential_put(id, None, req, state) } +// Get and return a single str +fn account_get_id_radius( + (path, req, state): (Path, HttpRequest, State), +) -> impl Future { + 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, HttpRequest, State), +) -> impl Future { + // 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, HttpRequest, State), +) -> impl Future { + json_rest_event_delete_id_attr(path, req, state, "radius_secret".to_string()) +} + +fn account_get_id_radius_token( + (path, req, state): (Path, HttpRequest, State), +) -> impl Future { + let uat = get_current_user(&req); + let id = path.into_inner(); + + let obj = InternalRadiusTokenReadMessage { + uat: uat, + uuid_or_name: id, + }; + + let res = state.qe_r.send(obj).from_err().and_then(|res| match res { + Ok(event_result) => { + // Only send back the first result, or None + Ok(HttpResponse::Ok().json(event_result)) + } + Err(e) => Ok(operation_error_to_response(e)), + }); + + Box::new(res) +} + fn group_get( (req, state): (HttpRequest, State), ) -> impl Future { let filter = filter_all!(f_eq("class", PartialValue::new_class("group"))); - json_rest_event_get(req, state, filter) + json_rest_event_get(req, state, filter, None) } fn group_id_get( (path, req, state): (Path, HttpRequest, State), ) -> impl Future { 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, State)) -> String { @@ -972,11 +1129,19 @@ pub fn create_server_core(config: Configuration) { // Can we self lock? }) .resource("/v1/self/_radius", |r| { + // Get our radius secret for manual configuration r.method(http::Method::GET).with(do_nothing) - // more to be added + }) + .resource("/v1/self/_radius", |r| { + // delete our radius secret + r.method(http::Method::DELETE).with(do_nothing) + }) + .resource("/v1/self/_radius", |r| { + // regenerate our radius secret + r.method(http::Method::POST).with(do_nothing) }) .resource("/v1/self/_radius/_config", |r| { - // Create new secret_otp? + // Create new secret_otp for client configuration r.method(http::Method::POST).with(do_nothing) }) .resource("/v1/self/_radius/_config/{secret_otp}", |r| { @@ -1019,11 +1184,17 @@ pub fn create_server_core(config: Configuration) { // add post, delete }) .resource("/v1/account/{id}/_radius", |r| { - r.method(http::Method::GET).with(do_nothing) - // more to be added + r.method(http::Method::GET) + .with_async(account_get_id_radius); + r.method(http::Method::POST) + .with_async(account_post_id_radius_regenerate); + r.method(http::Method::DELETE) + .with_async(account_delete_id_radius); }) + // This is how the radius server views a json blob about the ID and radius creds. .resource("/v1/account/{id}/_radius/_token", |r| { - r.method(http::Method::GET).with(do_nothing) + r.method(http::Method::GET) + .with_async(account_get_id_radius_token) }) // Groups .resource("/v1/group", |r| { diff --git a/kanidmd/src/lib/entry.rs b/kanidmd/src/lib/entry.rs index 69d3a26ff..30a6dc869 100644 --- a/kanidmd/src/lib/entry.rs +++ b/kanidmd/src/lib/entry.rs @@ -162,7 +162,9 @@ pub struct EntryInvalid; // pub struct EntryNormalised; #[derive(Clone, Copy, Debug)] -pub struct EntryReduced; +pub struct EntryReduced { + uuid: Uuid, +} #[derive(Debug)] pub struct Entry { @@ -980,10 +982,11 @@ impl Entry { }) } - #[cfg(test)] - pub fn to_reduced(self) -> Entry { + pub unsafe fn to_reduced(self) -> Entry { Entry { - valid: EntryReduced, + valid: EntryReduced { + uuid: self.valid.uuid, + }, state: self.state, attrs: self.attrs, } @@ -996,7 +999,7 @@ impl Entry { // Remove all attrs from our tree that are NOT in the allowed set. let Entry { - valid: _s_valid, + valid: s_valid, state: s_state, attrs: s_attrs, } = self; @@ -1013,135 +1016,11 @@ impl Entry { .collect(); Entry { - valid: EntryReduced, + valid: EntryReduced { uuid: s_valid.uuid }, state: s_state, attrs: f_attrs, } } - - // These are special types to allow returning typed values from - // an entry, if we "know" what we expect to receive. - - /// This returns an array of IndexTypes, when the type is an Optional - /// multivalue in schema - IE this will *not* fail if the attribute is - /// empty, yielding and empty array instead. - /// - /// However, the converstion to IndexType is fallaible, so in case of a failure - /// to convert, an Err is returned. - pub(crate) fn get_ava_opt_index(&self, attr: &str) -> Result, ()> { - match self.attrs.get(attr) { - Some(av) => { - let r: Result, _> = 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 { - 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> { - // If any value is NOT a reference, return none! - match self.attrs.get(attr) { - Some(av) => { - let v: Option> = 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> { - 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> { - match self.attrs.get(attr) { - Some(a) => { - let r: Vec = 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> { - match self.attrs.get(attr) { - Some(a) => { - let r: Vec = 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 { - 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 { - 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 Entry { @@ -1244,6 +1123,10 @@ impl Entry { } impl Entry { + pub fn get_uuid(&self) -> &Uuid { + &self.valid.uuid + } + pub fn into_pe( &self, audit: &mut AuditScope, @@ -1316,6 +1199,150 @@ impl Entry { r } + pub fn get_ava_reference_uuid(&self, attr: &str) -> Option> { + // If any value is NOT a reference, return none! + match self.attrs.get(attr) { + Some(av) => { + let v: Option> = 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, ()> { + match self.attrs.get(attr) { + Some(av) => { + let r: Result, _> = 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 { + 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> { + 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> { + match self.attrs.get(attr) { + Some(a) => { + let r: Vec = 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> { + match self.attrs.get(attr) { + Some(a) => { + let r: Vec = 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> { + match self.attrs.get(attr) { + Some(a) => { + let r: BTreeSet = 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 { + 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 { + self.get_ava_single(attr) + .and_then(|v: &Value| { + debug!("get_ava_single_protofilter -> {:?}", v); + v.as_json_filter() + }) + .and_then(|f: &ProtoFilter| Some((*f).clone())) + } + pub fn attribute_pres(&self, attr: &str) -> bool { // Note, we don't normalise attr name, but I think that's not // something we should over-optimise on. diff --git a/kanidmd/src/lib/event.rs b/kanidmd/src/lib/event.rs index 5db844e11..c57fceb4d 100644 --- a/kanidmd/src/lib/event.rs +++ b/kanidmd/src/lib/event.rs @@ -1,6 +1,8 @@ use crate::audit::AuditScope; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, EntryValid}; use crate::filter::{Filter, FilterValid}; +use crate::schema::SchemaTransaction; +use crate::value::PartialValue; use kanidm_proto::v1::Entry as ProtoEntry; use kanidm_proto::v1::{ AuthCredential, AuthResponse, AuthState, AuthStep, SearchResponse, UserAuthToken, @@ -25,6 +27,7 @@ use crate::filter::FilterInvalid; use crate::modify::ModifyInvalid; use actix::prelude::*; +use std::collections::BTreeSet; use uuid::Uuid; #[derive(Debug)] @@ -217,7 +220,7 @@ pub struct SearchEvent { pub filter: Filter, // This is the original filter, for the purpose of ACI checking. pub filter_orig: Filter, - // TODO #83: Add list of attributes to request + pub attrs: Option>, } impl SearchEvent { @@ -239,6 +242,9 @@ impl SearchEvent { filter_orig: f .validate(qs.get_schema()) .map_err(|e| OperationError::SchemaViolation(e))?, + // We can't get this from the SearchMessage because it's annoying with the + // current macro design. + attrs: None, }), Err(e) => Err(e), } @@ -249,6 +255,21 @@ impl SearchEvent { msg: InternalSearchMessage, qs: &QueryServerReadTransaction, ) -> Result { + let r_attrs: Option> = msg.attrs.map(|vs| { + vs.into_iter() + .filter_map(|a| qs.get_schema().normalise_attr_if_exists(a.as_str())) + .collect() + }); + + match &r_attrs { + Some(s) => { + if s.len() == 0 { + return Err(OperationError::EmptyRequest); + } + } + _ => {} + } + Ok(SearchEvent { event: Event::from_ro_uat(audit, qs, msg.uat)?, // We do need to do this twice to account for the ignore_hidden @@ -263,6 +284,7 @@ impl SearchEvent { .filter .validate(qs.get_schema()) .map_err(|e| OperationError::SchemaViolation(e))?, + attrs: r_attrs, }) } @@ -279,6 +301,26 @@ impl SearchEvent { filter_orig: filter_all!(f_self()) .validate(qs.get_schema()) .map_err(|e| OperationError::SchemaViolation(e))?, + // TODO: Should we limit this? + attrs: None, + }) + } + + pub fn from_target_uuid_request( + audit: &mut AuditScope, + uat: Option, + target_uuid: Uuid, + qs: &QueryServerReadTransaction, + ) -> Result { + Ok(SearchEvent { + event: Event::from_ro_uat(audit, qs, uat)?, + filter: filter!(f_eq("uuid", PartialValue::new_uuid(target_uuid.clone()))) + .validate(qs.get_schema()) + .map_err(|e| OperationError::SchemaViolation(e))?, + filter_orig: filter_all!(f_eq("uuid", PartialValue::new_uuid(target_uuid))) + .validate(qs.get_schema()) + .map_err(|e| OperationError::SchemaViolation(e))?, + attrs: None, }) } @@ -289,6 +331,7 @@ impl SearchEvent { event: Event::from_impersonate_entry_ser(e), filter: filter.clone().to_valid(), filter_orig: filter.to_valid(), + attrs: None, } } @@ -301,6 +344,7 @@ impl SearchEvent { event: Event::from_impersonate_entry(e), filter: filter.clone().to_valid(), filter_orig: filter.to_valid(), + attrs: None, } } @@ -313,6 +357,7 @@ impl SearchEvent { event: Event::from_impersonate(event), filter: filter, filter_orig: filter_orig, + attrs: None, } } @@ -351,6 +396,7 @@ impl SearchEvent { event: Event::from_impersonate_entry(e), filter: filter.clone().to_recycled().to_valid(), filter_orig: filter.to_valid(), + attrs: None, } } @@ -364,6 +410,7 @@ impl SearchEvent { event: Event::from_impersonate_entry(e), filter: filter.clone().to_ignore_hidden().to_valid(), filter_orig: filter.to_valid(), + attrs: None, } } @@ -373,6 +420,7 @@ impl SearchEvent { event: Event::from_internal(), filter: filter.clone().to_valid(), filter_orig: filter.to_valid(), + attrs: None, } } @@ -381,6 +429,7 @@ impl SearchEvent { event: Event::from_internal(), filter: filter.clone(), filter_orig: filter, + attrs: None, } } } @@ -580,6 +629,31 @@ impl ModifyEvent { } } + pub fn from_target_uuid_attr_purge( + audit: &mut AuditScope, + uat: Option, + target_uuid: Uuid, + attr: String, + qs: &QueryServerWriteTransaction, + ) -> Result { + 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, modlist: ModifyList) -> Self { ModifyEvent { event: Event::from_internal(), diff --git a/kanidmd/src/lib/idm/account.rs b/kanidmd/src/lib/idm/account.rs index 0457f925a..b88fd736f 100644 --- a/kanidmd/src/lib/idm/account.rs +++ b/kanidmd/src/lib/idm/account.rs @@ -3,11 +3,13 @@ use kanidm_proto::v1::OperationError; use kanidm_proto::v1::UserAuthToken; +use crate::audit::AuditScope; use crate::constants::UUID_ANONYMOUS; use crate::credential::Credential; use crate::idm::claim::Claim; use crate::idm::group::Group; use crate::modify::{ModifyInvalid, ModifyList}; +use crate::server::{QueryServerReadTransaction, QueryServerWriteTransaction}; use crate::value::{PartialValue, Value}; use uuid::Uuid; @@ -16,6 +18,46 @@ lazy_static! { static ref PVCLASS_ACCOUNT: PartialValue = PartialValue::new_class("account"); } +macro_rules! try_from_entry { + ($value:expr, $groups:expr) => {{ + // Check the classes + if !$value.attribute_value_pres("class", &PVCLASS_ACCOUNT) { + return Err(OperationError::InvalidAccountState( + "Missing class: account".to_string(), + )); + } + + // Now extract our needed attributes + let name = + $value + .get_ava_single_string("name") + .ok_or(OperationError::InvalidAccountState( + "Missing attribute: name".to_string(), + ))?; + + let displayname = $value.get_ava_single_string("displayname").ok_or( + OperationError::InvalidAccountState("Missing attribute: displayname".to_string()), + )?; + + let primary = $value + .get_ava_single_credential("primary_credential") + .map(|v| v.clone()); + + // Resolved by the caller + let groups = $groups; + + let uuid = $value.get_uuid().clone(); + + Ok(Account { + uuid: uuid, + name: name, + displayname: displayname, + groups: groups, + primary: primary, + }) + }}; +} + #[derive(Debug, Clone)] pub(crate) struct Account { // Later these could be &str if we cache entry here too ... @@ -35,45 +77,29 @@ pub(crate) struct Account { } impl Account { - // TODO #71: We need a second try_from that doesn't do group resolve for test cases I think. - pub(crate) fn try_from_entry( + pub(crate) fn try_from_entry_ro( + au: &mut AuditScope, + value: Entry, + qs: &QueryServerReadTransaction, + ) -> Result { + 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, + qs: &QueryServerWriteTransaction, + ) -> Result { + 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, ) -> Result { - // 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()); - - // 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, - }) + try_from_entry!(value, vec![]) } // Could this actually take a claims list and application instead? @@ -127,6 +153,14 @@ impl Account { } // no appid } } + + pub(crate) fn regenerate_radius_secret_mod( + &self, + cleartext: &str, + ) -> Result, OperationError> { + let vcred = Value::new_radius_str(cleartext); + Ok(ModifyList::new_purge_and_set("radius_secret", vcred)) + } } // Need to also add a "to UserAuthToken" ... @@ -145,7 +179,7 @@ mod tests { unsafe { Entry::unsafe_from_entry_str(JSON_ANONYMOUS_V1).to_valid_new() }; let anon_e = unsafe { anon_e.to_valid_committed() }; - let anon_account = Account::try_from_entry(anon_e).expect("Must not fail"); + let anon_account = Account::try_from_entry_no_groups(anon_e).expect("Must not fail"); println!("{:?}", anon_account); // I think that's it? we may want to check anonymous mech ... } diff --git a/kanidmd/src/lib/idm/event.rs b/kanidmd/src/lib/idm/event.rs index 3dbb3a39c..ba88c4c9e 100644 --- a/kanidmd/src/lib/idm/event.rs +++ b/kanidmd/src/lib/idm/event.rs @@ -1,7 +1,7 @@ use crate::actors::v1_write::IdmAccountSetPasswordMessage; use crate::audit::AuditScope; use crate::event::Event; -use crate::server::QueryServerWriteTransaction; +use crate::server::{QueryServerReadTransaction, QueryServerWriteTransaction}; use uuid::Uuid; @@ -84,3 +84,67 @@ impl GeneratePasswordEvent { }) } } + +#[derive(Debug)] +pub struct RegenerateRadiusSecretEvent { + pub event: Event, + pub target: Uuid, +} + +impl RegenerateRadiusSecretEvent { + pub fn from_parts( + audit: &mut AuditScope, + qs: &QueryServerWriteTransaction, + uat: Option, + target: Uuid, + ) -> Result { + 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, + target: Uuid, + ) -> Result { + 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, + } + } +} diff --git a/kanidmd/src/lib/idm/group.rs b/kanidmd/src/lib/idm/group.rs index cbcab2bb8..bf7547d7a 100644 --- a/kanidmd/src/lib/idm/group.rs +++ b/kanidmd/src/lib/idm/group.rs @@ -1,19 +1,111 @@ +use crate::audit::AuditScope; +use crate::entry::{Entry, EntryCommitted, EntryReduced, EntryValid}; +use crate::server::{ + QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction, +}; +use crate::value::PartialValue; use kanidm_proto::v1::Group as ProtoGroup; +use kanidm_proto::v1::OperationError; + +use uuid::Uuid; + +lazy_static! { + static ref PVCLASS_GROUP: PartialValue = PartialValue::new_class("group"); +} #[derive(Debug, Clone)] pub struct Group { - // name -// uuid + name: String, + uuid: Uuid, + // We'll probably add policy and claims later to this +} + +macro_rules! try_from_account_e { + ($au:expr, $value:expr, $qs:expr) => {{ + let groups: Vec = 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, _> = + ges.into_iter().map(|e| Group::try_from_entry(e)).collect(); + groups.map_err(|e| { + // log + e + })? + } + None => { + // No memberof, no groups! + vec![] + } + }; + Ok(groups) + }}; } impl Group { - /* - pub fn new() -> Self { - Group {} + pub fn try_from_account_entry_red_ro( + au: &mut AuditScope, + value: &Entry, + qs: &QueryServerReadTransaction, + ) -> Result, OperationError> { + try_from_account_e!(au, value, qs) + } + + pub fn try_from_account_entry_ro( + au: &mut AuditScope, + value: &Entry, + qs: &QueryServerReadTransaction, + ) -> Result, OperationError> { + try_from_account_e!(au, value, qs) + } + + pub fn try_from_account_entry_rw( + au: &mut AuditScope, + value: &Entry, + qs: &QueryServerWriteTransaction, + ) -> Result, OperationError> { + try_from_account_e!(au, value, qs) + } + + pub fn try_from_entry( + value: Entry, + ) -> Result { + if !value.attribute_value_pres("class", &PVCLASS_GROUP) { + return Err(OperationError::InvalidAccountState( + "Missing class: group".to_string(), + )); + } + + // Now extract our needed attributes + let name = + value + .get_ava_single_string("name") + .ok_or(OperationError::InvalidAccountState( + "Missing attribute: name".to_string(), + ))?; + + let uuid = value.get_uuid().clone(); + + Ok(Group { + name: name, + uuid: uuid, + }) } - */ pub fn into_proto(&self) -> ProtoGroup { - unimplemented!(); + ProtoGroup { + name: self.name.clone(), + uuid: self.uuid.to_hyphenated_ref().to_string(), + } } } diff --git a/kanidmd/src/lib/idm/macros.rs b/kanidmd/src/lib/idm/macros.rs index 26b90ef65..69d723a46 100644 --- a/kanidmd/src/lib/idm/macros.rs +++ b/kanidmd/src/lib/idm/macros.rs @@ -8,7 +8,7 @@ macro_rules! entry_str_to_account { unsafe { Entry::unsafe_from_entry_str($entry_str).to_valid_new() }; let e = unsafe { e.to_valid_committed() }; - Account::try_from_entry(e).expect("Account conversion failure") + Account::try_from_entry_no_groups(e).expect("Account conversion failure") }}; } diff --git a/kanidmd/src/lib/idm/mod.rs b/kanidmd/src/lib/idm/mod.rs index 36151c302..b8f1453c2 100644 --- a/kanidmd/src/lib/idm/mod.rs +++ b/kanidmd/src/lib/idm/mod.rs @@ -6,5 +6,6 @@ pub(crate) mod authsession; pub(crate) mod claim; pub(crate) mod event; pub(crate) mod group; +pub(crate) mod radius; pub(crate) mod server; // mod identity; diff --git a/kanidmd/src/lib/idm/radius.rs b/kanidmd/src/lib/idm/radius.rs new file mode 100644 index 000000000..e56608fab --- /dev/null +++ b/kanidmd/src/lib/idm/radius.rs @@ -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, + pub radius_secret: String, +} + +impl RadiusAccount { + pub(crate) fn try_from_entry_reduced( + au: &mut AuditScope, + value: Entry, + qs: &QueryServerReadTransaction, + ) -> Result { + 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 { + // 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(), + }) + } +} diff --git a/kanidmd/src/lib/idm/server.rs b/kanidmd/src/lib/idm/server.rs index 80249b1d8..70c45f0a0 100644 --- a/kanidmd/src/lib/idm/server.rs +++ b/kanidmd/src/lib/idm/server.rs @@ -3,13 +3,18 @@ use crate::constants::AUTH_SESSION_TIMEOUT; use crate::event::{AuthEvent, AuthEventStep, AuthResult}; use crate::idm::account::Account; use crate::idm::authsession::AuthSession; -use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent}; +use crate::idm::event::{ + GeneratePasswordEvent, PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent, +}; +use crate::idm::radius::RadiusAccount; +use crate::server::QueryServerReadTransaction; use crate::server::{QueryServer, QueryServerTransaction, QueryServerWriteTransaction}; -use crate::utils::{password_from_random, uuid_from_duration, SID}; +use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, SID}; use crate::value::PartialValue; use kanidm_proto::v1::AuthState; use kanidm_proto::v1::OperationError; +use kanidm_proto::v1::RadiusAuthToken; use concread::cowcell::{CowCell, CowCellWriteTxn}; use std::collections::BTreeMap; @@ -21,9 +26,6 @@ pub struct IdmServer { // means that limits to sessions can be easily applied and checked to // variaous accounts, and we have a good idea of how to structure the // in memory caches related to locking. - // - // TODO #60: This needs a mark-and-sweep gc to be added. - // use split_off() sessions: CowCell>, // Need a reference to the query server. qs: QueryServer, @@ -40,13 +42,11 @@ pub struct IdmServerWriteTransaction<'a> { sid: &'a SID, } -/* -pub struct IdmServerReadTransaction<'a> { +pub struct IdmServerProxyReadTransaction { // This contains read-only methods, like getting users, groups // and other structured content. - qs: &'a QueryServer, + pub qs_read: QueryServerReadTransaction, } -*/ pub struct IdmServerProxyWriteTransaction<'a> { // This does NOT take any read to the memory content, allowing safe @@ -72,11 +72,11 @@ impl IdmServer { } } - /* - pub fn read(&self) -> IdmServerReadTransaction { - IdmServerReadTransaction { qs: &self.qs } + pub fn proxy_read(&self) -> IdmServerProxyReadTransaction { + IdmServerProxyReadTransaction { + qs_read: self.qs.read(), + } } - */ pub fn proxy_write(&self) -> IdmServerProxyWriteTransaction { IdmServerProxyWriteTransaction { @@ -113,8 +113,7 @@ impl<'a> IdmServerWriteTransaction<'a> { match &ae.step { AuthEventStep::Init(init) => { - // Allocate a session id. - // TODO: #60 - make this new_v1 and use the tstamp. + // Allocate a session id, based on current time. let sessionid = uuid_from_duration(ct, self.sid); // Begin the auth procedure! @@ -161,7 +160,7 @@ impl<'a> IdmServerWriteTransaction<'a> { // typing and functionality so we can assess what auth types can // continue, and helps to keep non-needed entry specific data // out of the LRU. - let account = Account::try_from_entry(entry)?; + let account = Account::try_from_entry_ro(au, entry, &qs_read)?; let auth_session = AuthSession::new(account, init.appid.clone()); // Get the set of mechanisms that can proceed. This is tied @@ -213,11 +212,26 @@ impl<'a> IdmServerWriteTransaction<'a> { } } -/* -impl<'a> IdmServerReadTransaction<'a> { - pub fn whoami() -> () {} +impl IdmServerProxyReadTransaction { + pub fn get_radiusauthtoken( + &self, + au: &mut AuditScope, + rate: &RadiusAuthTokenEvent, + ) -> Result { + // TODO: This needs to be an impersonate search! + let account_entry = try_audit!( + au, + self.qs_read + .impersonate_search_ext_uuid(au, &rate.target, &rate.event) + ); + let account = try_audit!( + au, + RadiusAccount::try_from_entry_reduced(au, account_entry, &self.qs_read) + ); + + account.to_radiusauthtoken() + } } -*/ impl<'a> IdmServerProxyWriteTransaction<'a> { pub fn set_account_password( @@ -227,7 +241,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { ) -> Result<(), OperationError> { // Get the account let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &pce.target)); - let account = try_audit!(au, Account::try_from_entry(account_entry)); + let account = try_audit!( + au, + Account::try_from_entry_rw(au, account_entry, &self.qs_write) + ); // Ask if tis all good - this step checks pwpolicy and such // Deny the change if the account is anonymous! @@ -289,7 +306,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { ) -> Result { // Get the account let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &gpe.target)); - let account = try_audit!(au, Account::try_from_entry(account_entry)); + let account = try_audit!( + au, + Account::try_from_entry_rw(au, account_entry, &self.qs_write) + ); // Ask if tis all good - this step checks pwpolicy and such // Deny the change if the target account is anonymous! @@ -318,6 +338,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { // Filter as intended (acp) filter_all!(f_eq("uuid", PartialValue::new_uuidr(&gpe.target))), modlist, + // Provide the event to impersonate &gpe.event, ) ); @@ -325,6 +346,48 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { Ok(cleartext) } + pub fn regenerate_radius_secret( + &mut self, + au: &mut AuditScope, + rrse: &RegenerateRadiusSecretEvent, + ) -> Result { + // regenerates and returns the radius secret + let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &rrse.target)); + let account = try_audit!( + au, + Account::try_from_entry_rw(au, account_entry, &self.qs_write) + ); + // Deny the change if the target account is anonymous! + if account.is_anonymous() { + return Err(OperationError::SystemProtectedObject); + } + + // Difference to the password above, this is intended to be read/copied + // by a human wiath a keyboard in some cases. + let cleartext = readable_password_from_random(); + + // Create a modlist from the change. + let modlist = try_audit!(au, account.regenerate_radius_secret_mod(cleartext.as_str())); + audit_log!(au, "processing change {:?}", modlist); + + // Apply it. + try_audit!( + au, + self.qs_write.impersonate_modify( + au, + // Filter as executed + filter!(f_eq("uuid", PartialValue::new_uuidr(&rrse.target))), + // Filter as intended (acp) + filter_all!(f_eq("uuid", PartialValue::new_uuidr(&rrse.target))), + modlist, + // Provide the event to impersonate + &rrse.event, + ) + ); + + Ok(cleartext) + } + pub fn commit(self, au: &mut AuditScope) -> Result<(), OperationError> { self.qs_write.commit(au) } @@ -337,7 +400,9 @@ mod tests { use crate::constants::{AUTH_SESSION_TIMEOUT, UUID_ADMIN, UUID_ANONYMOUS}; use crate::credential::Credential; use crate::event::{AuthEvent, AuthResult, ModifyEvent}; - use crate::idm::event::PasswordChangeEvent; + use crate::idm::event::{ + PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent, + }; use crate::modify::{Modify, ModifyList}; use crate::value::{PartialValue, Value}; use kanidm_proto::v1::OperationError; @@ -633,4 +698,43 @@ mod tests { assert!(!idms_write.is_sessionid_present(&sid)); }) } + + #[test] + fn test_idm_regenerate_radius_secret() { + run_idm_test!(|_qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| { + let mut idms_prox_write = idms.proxy_write(); + let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_ADMIN.clone()); + + // Generates a new credential when none exists + let r1 = idms_prox_write + .regenerate_radius_secret(au, &rrse) + .expect("Failed to reset radius credential 1"); + // Regenerates and overwrites the radius credential + let r2 = idms_prox_write + .regenerate_radius_secret(au, &rrse) + .expect("Failed to reset radius credential 2"); + assert!(r1 != r2); + }) + } + + #[test] + fn test_idm_radiusauthtoken() { + run_idm_test!(|_qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| { + let mut idms_prox_write = idms.proxy_write(); + let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_ADMIN.clone()); + let r1 = idms_prox_write + .regenerate_radius_secret(au, &rrse) + .expect("Failed to reset radius credential 1"); + idms_prox_write.commit(au).expect("failed to commit"); + + let idms_prox_read = idms.proxy_read(); + let rate = RadiusAuthTokenEvent::new_internal(UUID_ADMIN.clone()); + let tok_r = idms_prox_read + .get_radiusauthtoken(au, &rate) + .expect("Failed to generate radius auth token"); + + // view the token? + assert!(r1 == tok_r.secret); + }) + } } diff --git a/kanidmd/src/lib/lib.rs b/kanidmd/src/lib/lib.rs index 80b1b1e3a..94c1f9c96 100644 --- a/kanidmd/src/lib/lib.rs +++ b/kanidmd/src/lib/lib.rs @@ -1,4 +1,4 @@ -#![deny(warnings)] +// #![deny(warnings)] #![warn(unused_extern_crates)] #[macro_use] diff --git a/kanidmd/src/lib/modify.rs b/kanidmd/src/lib/modify.rs index 8a12bb0bf..4577424fc 100644 --- a/kanidmd/src/lib/modify.rs +++ b/kanidmd/src/lib/modify.rs @@ -91,6 +91,10 @@ impl ModifyList { Self::new_list(vec![m_purge(attr), Modify::Present(attr.to_string(), v)]) } + pub fn new_purge(attr: &str) -> Self { + Self::new_list(vec![m_purge(attr)]) + } + pub fn push_mod(&mut self, modify: Modify) { self.mods.push(modify) } diff --git a/kanidmd/src/lib/schema.rs b/kanidmd/src/lib/schema.rs index 5384dd466..4401a01bd 100644 --- a/kanidmd/src/lib/schema.rs +++ b/kanidmd/src/lib/schema.rs @@ -214,6 +214,22 @@ impl SchemaAttribute { } } + fn validate_radius_utf8string(&self, v: &Value) -> Result<(), SchemaError> { + if v.is_radius_string() { + Ok(()) + } else { + Err(SchemaError::InvalidAttributeSyntax) + } + } + + fn validate_sshkey(&self, v: &Value) -> Result<(), SchemaError> { + if v.is_sshkey() { + Ok(()) + } else { + Err(SchemaError::InvalidAttributeSyntax) + } + } + // TODO: There may be a difference between a value and a filter value on complex // types - IE a complex type may have multiple parts that are secret, but a filter // on that may only use a single tagged attribute for example. @@ -228,6 +244,8 @@ impl SchemaAttribute { SyntaxType::UTF8STRING => v.is_utf8(), SyntaxType::JSON_FILTER => v.is_json_filter(), SyntaxType::CREDENTIAL => v.is_credential(), + SyntaxType::RADIUS_UTF8STRING => v.is_radius_string(), + SyntaxType::SSHKEY => v.is_sshkey(), }; if r { Ok(()) @@ -322,6 +340,20 @@ impl SchemaAttribute { acc } }), + SyntaxType::RADIUS_UTF8STRING => ava.iter().fold(Ok(()), |acc, v| { + if acc.is_ok() { + self.validate_radius_utf8string(v) + } else { + acc + } + }), + SyntaxType::SSHKEY => ava.iter().fold(Ok(()), |acc, v| { + if acc.is_ok() { + self.validate_sshkey(v) + } else { + acc + } + }), } } } @@ -435,6 +467,14 @@ pub trait SchemaTransaction { an.to_lowercase() } + fn normalise_attr_if_exists(&self, an: &str) -> Option { + if self.get_attributes().contains_key(an) { + Some(self.normalise_attr_name(an)) + } else { + None + } + } + fn get_attributes_unique(&self) -> Vec { // This could be improved by caching this set on schema reload! self.get_attributes() diff --git a/kanidmd/src/lib/server.rs b/kanidmd/src/lib/server.rs index 07a3b6d47..05deba600 100644 --- a/kanidmd/src/lib/server.rs +++ b/kanidmd/src/lib/server.rs @@ -295,6 +295,20 @@ pub trait QueryServerTransaction { res } + fn impersonate_search_ext_valid( + &self, + audit: &mut AuditScope, + f_valid: Filter, + f_intent_valid: Filter, + event: &Event, + ) -> Result>, OperationError> { + let se = SearchEvent::new_impersonate(event, f_valid, f_intent_valid); + let mut audit_int = AuditScope::new("impersonate_search_ext"); + let res = self.search_ext(&mut audit_int, &se); + audit.append_scope(audit_int); + res + } + // Who they are will go here fn impersonate_search( &self, @@ -312,6 +326,22 @@ pub trait QueryServerTransaction { self.impersonate_search_valid(audit, f_valid, f_intent_valid, event) } + fn impersonate_search_ext( + &self, + audit: &mut AuditScope, + filter: Filter, + filter_intent: Filter, + event: &Event, + ) -> Result>, OperationError> { + let f_valid = filter + .validate(self.get_schema()) + .map_err(|e| OperationError::SchemaViolation(e))?; + let f_intent_valid = filter_intent + .validate(self.get_schema()) + .map_err(|e| OperationError::SchemaViolation(e))?; + self.impersonate_search_ext_valid(audit, f_valid, f_intent_valid, event) + } + // Get a single entry by it's UUID. This is heavily relied on for internal // server operations, especially in login and acp checks for acp. fn internal_search_uuid( @@ -340,6 +370,28 @@ pub trait QueryServerTransaction { } } + fn impersonate_search_ext_uuid( + &self, + audit: &mut AuditScope, + uuid: &Uuid, + event: &Event, + ) -> Result, OperationError> { + let filter_intent = filter_all!(f_eq("uuid", PartialValue::new_uuid(uuid.clone()))); + let filter = filter!(f_eq("uuid", PartialValue::new_uuid(uuid.clone()))); + let res = self.impersonate_search_ext(audit, filter, filter_intent, event); + match res { + Ok(vs) => { + if vs.len() > 1 { + return Err(OperationError::NoMatchingEntries); + } + vs.into_iter() + .next() + .ok_or(OperationError::NoMatchingEntries) + } + Err(e) => Err(e), + } + } + /// Do a schema aware conversion from a String:String to String:Value for modification /// present. fn clone_value( @@ -403,6 +455,8 @@ pub trait QueryServerTransaction { SyntaxType::JSON_FILTER => Value::new_json_filter(value) .ok_or(OperationError::InvalidAttribute("Invalid Filter syntax".to_string())), SyntaxType::CREDENTIAL => Err(OperationError::InvalidAttribute("Credentials can not be supplied through modification - please use the IDM api".to_string())), + SyntaxType::RADIUS_UTF8STRING => Err(OperationError::InvalidAttribute("Radius secrets can not be supplied through modification - please use the IDM api".to_string())), + SyntaxType::SSHKEY => Err(OperationError::InvalidAttribute("SSH public keys can not be supplied through modification - please use the IDM api".to_string())), } } None => { @@ -475,6 +529,8 @@ pub trait QueryServerTransaction { OperationError::InvalidAttribute("Invalid Filter syntax".to_string()), ), SyntaxType::CREDENTIAL => Ok(PartialValue::new_credential_tag(value.as_str())), + SyntaxType::RADIUS_UTF8STRING => Ok(PartialValue::new_radius_string()), + SyntaxType::SSHKEY => Ok(PartialValue::new_sshkey_tag_s(value.as_str())), } } None => { @@ -1538,6 +1594,7 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_SSH_PUBLICKEY, JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL, + JSON_SCHEMA_ATTR_RADIUS_SECRET, JSON_SCHEMA_CLASS_PERSON, JSON_SCHEMA_CLASS_GROUP, JSON_SCHEMA_CLASS_ACCOUNT, diff --git a/kanidmd/src/lib/utils.rs b/kanidmd/src/lib/utils.rs index ccfc8e592..f6bef7e3a 100644 --- a/kanidmd/src/lib/utils.rs +++ b/kanidmd/src/lib/utils.rs @@ -25,6 +25,17 @@ pub fn password_from_random() -> String { rand_string } +pub fn readable_password_from_random() -> String { + let mut trng = thread_rng(); + format!( + "{}-{}-{}-{}", + trng.sample_iter(&Alphanumeric).take(4).collect::(), + trng.sample_iter(&Alphanumeric).take(4).collect::(), + trng.sample_iter(&Alphanumeric).take(4).collect::(), + trng.sample_iter(&Alphanumeric).take(4).collect::(), + ) +} + #[allow(dead_code)] pub fn uuid_from_now(sid: &SID) -> Uuid { let d = SystemTime::now() diff --git a/kanidmd/src/lib/value.rs b/kanidmd/src/lib/value.rs index 498262c7a..892c9cfe6 100644 --- a/kanidmd/src/lib/value.rs +++ b/kanidmd/src/lib/value.rs @@ -1,4 +1,4 @@ -use crate::be::dbvalue::{DbValueCredV1, DbValueV1}; +use crate::be::dbvalue::{DbValueCredV1, DbValueTaggedStringV1, DbValueV1}; use crate::credential::Credential; use kanidm_proto::v1::Filter as ProtoFilter; @@ -84,6 +84,8 @@ pub enum SyntaxType { REFERENCE_UUID, JSON_FILTER, CREDENTIAL, + RADIUS_UTF8STRING, + SSHKEY, } impl TryFrom<&str> for SyntaxType { @@ -101,6 +103,8 @@ impl TryFrom<&str> for SyntaxType { "REFERENCE_UUID" => Ok(SyntaxType::REFERENCE_UUID), "JSON_FILTER" => Ok(SyntaxType::JSON_FILTER), "CREDENTIAL" => Ok(SyntaxType::CREDENTIAL), + "RADIUS_UTF8STRING" => Ok(SyntaxType::RADIUS_UTF8STRING), + "SSHKEY" => Ok(SyntaxType::SSHKEY), _ => Err(()), } } @@ -120,6 +124,8 @@ impl TryFrom for SyntaxType { 6 => Ok(SyntaxType::REFERENCE_UUID), 7 => Ok(SyntaxType::JSON_FILTER), 8 => Ok(SyntaxType::CREDENTIAL), + 9 => Ok(SyntaxType::RADIUS_UTF8STRING), + 10 => Ok(SyntaxType::SSHKEY), _ => Err(()), } } @@ -137,6 +143,8 @@ impl SyntaxType { SyntaxType::REFERENCE_UUID => "REFERENCE_UUID", SyntaxType::JSON_FILTER => "JSON_FILTER", SyntaxType::CREDENTIAL => "CREDENTIAL", + SyntaxType::RADIUS_UTF8STRING => "RADIUS_UTF8STRING", + SyntaxType::SSHKEY => "SSHKEY", }) } @@ -151,6 +159,8 @@ impl SyntaxType { SyntaxType::REFERENCE_UUID => 6, SyntaxType::JSON_FILTER => 7, SyntaxType::CREDENTIAL => 8, + SyntaxType::RADIUS_UTF8STRING => 9, + SyntaxType::SSHKEY => 10, } } } @@ -158,8 +168,8 @@ impl SyntaxType { #[derive(Debug, Clone)] pub enum DataValue { Cred(Credential), - // SshKey(String), - // RadiusCred(String), + SshKey(String), + RadiusCred(String), } #[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Deserialize, Serialize)] @@ -176,8 +186,8 @@ pub enum PartialValue { JsonFilt(ProtoFilter), // Tag, matches to a DataValue. Cred(String), - // SshKey(String), - // RadiusCred(String), + SshKey(String), + RadiusCred, } impl PartialValue { @@ -337,6 +347,28 @@ impl PartialValue { } } + pub fn new_radius_string() -> Self { + PartialValue::RadiusCred + } + + pub fn is_radius_string(&self) -> bool { + match self { + PartialValue::RadiusCred => true, + _ => false, + } + } + + pub fn new_sshkey_tag_s(s: &str) -> Self { + PartialValue::SshKey(s.to_string()) + } + + pub fn is_sshkey(&self) -> bool { + match self { + PartialValue::SshKey(_) => true, + _ => false, + } + } + pub fn to_str(&self) -> Option<&str> { match self { PartialValue::Utf8(s) => Some(s.as_str()), @@ -368,6 +400,9 @@ impl PartialValue { serde_json::to_string(s).expect("A json filter value was corrupted during run-time") } PartialValue::Cred(tag) => tag.to_string(), + // This will never match as we never index radius creds! See generate_idx_eq_keys + PartialValue::RadiusCred => "_".to_string(), + PartialValue::SshKey(tag) => tag.to_string(), } } @@ -692,6 +727,7 @@ impl Value { PartialValue::Cred(_) => match &self.data { Some(dv) => match dv { DataValue::Cred(c) => Some(&c), + _ => None, }, None => None, }, @@ -699,6 +735,40 @@ impl Value { } } + pub fn new_radius_str(cleartext: &str) -> Self { + Value { + pv: PartialValue::new_radius_string(), + data: Some(DataValue::RadiusCred(cleartext.to_string())), + } + } + + pub fn is_radius_string(&self) -> bool { + match &self.pv { + PartialValue::RadiusCred => true, + _ => false, + } + } + + pub fn get_radius_secret(&self) -> Option<&str> { + match &self.pv { + PartialValue::RadiusCred => match &self.data { + Some(dv) => match dv { + DataValue::RadiusCred(c) => Some(c.as_str()), + _ => None, + }, + _ => None, + }, + _ => None, + } + } + + pub fn is_sshkey(&self) -> bool { + match &self.pv { + PartialValue::SshKey(_) => true, + _ => false, + } + } + pub fn contains(&self, s: &PartialValue) -> bool { self.pv.contains(s) } @@ -756,6 +826,14 @@ impl Value { data: Some(DataValue::Cred(Credential::try_from(dvc.d)?)), }) } + DbValueV1::RU(d) => Ok(Value { + pv: PartialValue::RadiusCred, + data: Some(DataValue::RadiusCred(d)), + }), + DbValueV1::SK(ts) => Ok(Value { + pv: PartialValue::SshKey(ts.t), + data: Some(DataValue::SshKey(ts.d)), + }), } } @@ -776,12 +854,10 @@ impl Value { PartialValue::Cred(tag) => { // Get the credential out and make sure it matches the type we expect. let c = match &self.data { - Some(v) => { - match &v { - DataValue::Cred(c) => c, - // _ => panic!(), - } - } + Some(v) => match &v { + DataValue::Cred(c) => c, + _ => panic!(), + }, None => panic!(), }; @@ -791,6 +867,29 @@ impl Value { d: c.to_db_valuev1(), }) } + PartialValue::RadiusCred => { + let ru = match &self.data { + Some(v) => match &v { + DataValue::RadiusCred(rc) => rc.clone(), + _ => panic!(), + }, + None => panic!(), + }; + DbValueV1::RU(ru) + } + PartialValue::SshKey(t) => { + let sk = match &self.data { + Some(v) => match &v { + DataValue::SshKey(sc) => sc.clone(), + _ => panic!(), + }, + None => panic!(), + }; + DbValueV1::SK(DbValueTaggedStringV1 { + t: t.clone(), + d: sk, + }) + } } } @@ -876,6 +975,8 @@ impl Value { // tag to the proto side. The credentials private data is stored seperately. tag.to_string() } + PartialValue::SshKey(tag) => tag.to_string(), + PartialValue::RadiusCred => "radius".to_string(), } } @@ -885,12 +986,24 @@ impl Value { // data. match &self.pv { PartialValue::Cred(_) => match &self.data { - Some(v) => { - match &v { - DataValue::Cred(_) => true, - // _ => false, - } - } + Some(v) => match &v { + DataValue::Cred(_) => true, + _ => false, + }, + None => false, + }, + PartialValue::SshKey(_) => match &self.data { + Some(v) => match &v { + DataValue::SshKey(_) => true, + _ => false, + }, + None => false, + }, + PartialValue::RadiusCred => match &self.data { + Some(v) => match &v { + DataValue::RadiusCred(_) => true, + _ => false, + }, None => false, }, _ => true, @@ -909,6 +1022,8 @@ impl Value { PartialValue::JsonFilt(s) => vec![serde_json::to_string(s) .expect("A json filter value was corrupted during run-time")], PartialValue::Cred(tag) => vec![tag.to_string()], + PartialValue::SshKey(tag) => vec![tag.to_string()], + PartialValue::RadiusCred => vec![], } } }