6 idm api design (#109)

Draft of the idm server rest api layout. This is no means a final representation of what this API will look like, but it's important that the ideas and direction, as well as capabilities were documented and discussed.
This commit is contained in:
Firstyear 2019-09-30 19:01:20 +10:00 committed by GitHub
parent e9cb71b9a7
commit 4ba34d18e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 501 additions and 9 deletions

1
.gitignore vendored
View file

@ -2,5 +2,6 @@
.DS_Store
.backup_test.db
/target
/insecure
**/*.rs.bk
test.db

316
designs/idm_rest_layout.rst Normal file
View file

@ -0,0 +1,316 @@
IDM API Design and Layout
-------------------------
To think about this, we need to look at what the eventual structure of the CLI will look like as
it roughly maps to the same operations.
The cli layout will be (roughly, not actual final product):
::
- raw
| - search
| - create
| - modify
| - delete
- recycle_bin
| - list
| - display (view, get)
| - search
| - revive (is restore better)
- self
| - display
| - set_credential (--appid/primary, --password, --totp, --webauthn, or combinations?)
| - reset_radius_password
| - add_credential_claim
| - remove_credential_claim
| - set_name
| - set_displayname
| - set_legalname
| - add_sshpublickey
| - remove_sshpublickey
| - modify (with --arg, could we get this from schema)
| - get_radius_android
| - get_radius_ios_macos
| - get_radius_config
- account
| - list
| - display
| - create
| - delete
| - modify
| - reset_credential
| - add_credential_claim
| - remove_credential_claim
| - set_name
| - set_displayname
| - set_legalname
| - enroll_sshpublickey
| - remove_sshpublickey
| - lock
| - unlock
| - expire_at
- group
| - list
| - display
| - create
| - delete
| - modify
| - add_members
| - remove_members
- claims
| - list
| - display
| - create
| - delete
| - modify
| - set_credential_policy
- access_profiles
| - list
| - display
| - create
| - delete
| - modify
| - enable
| - disable
- schema
| - class
| - list
| - get
| - create
| - add_may_attribute
| - add_must_attribute
| - attribute
| - list
| - get
| - create
| - query_class (--may, --must)
- radius
| - TBD
To support this, I think we need to break the api resources down in a similar pattern. We'd need
to think about how this looks with rest ...
raw
===
This is the server's internal CRUD (well, CSMD) protocol exposed at a low level
for batch operations if required. We could simply have /raw take a list of
the CUD/CMD ops for a real batching system ...
::
/v1/raw/search
POST -> search request
/v1/raw/create
POST -> create request
/v1/raw/modify
POST -> modify request
/v1/raw/delete
POST -> modify request
account
=======
::
/v1/account/
GET -> list all account ids
POST -> create new account
/v1/account/{id}
GET -> display account
PUT -> overwrite account attrs
PATCH -> update via diff
DELETE -> delete this account
/v1/account/{id}/_attr/{attr}
GET -> display this attr
PUT -> overwrite this attr value list
POST -> append this list to attr
DELETE -> purge this attr
/v1/account/{id}/_lock
POST -> lock this account until time (or null for permanent)
DELETE -> unlock this account
/v1/account/{id}/_credential
GET -> list the credentials
DELETE ->
/v1/account/{id}/_credential/{id}/_lock
POST -> lock this credential until time (or null for permament)
DELETE -> unlock this account
/v1/account/{id}/_radius
GET -> get the accounts radius credentials
(note: more methods to come to update/reset this credential
/v1/account/{id}/_radius/_token
GET -> let's the radius server get all required details for radius to work
self
====
Modify and perform actions on self - generally this is an extension of capability
from account and person, but combined to one.
::
/v1/self/
GET -> view self (aka whoami)
PUT -> overwrite self content
PATCH -> update self via diff
/v1/self/_attr/{attr}
GET -> view self attribute.
PUT -> overwrite attr
POST -> append list of attr
DELETE -> purge attr
/v1/self/_credential
(note: more to come re setting/updating credentials, see account)
/v1/self/_radius/
GET -> list radius cred
(note: more to come re setting/updating this credential)
/v1/self/_radius/_config
POST -> create new config link w_ secret key?
/v1/self/_radius/_config/{secret_key}/
GET -> get radius config json (no auth needed, secret_key is OTP)
/v1/self/_radius/_config/{secret_key}/apple
GET -> get radius config profile for apple (secret_key is OTP)
/v1/self/_radius/_config/{secret_key}/android
GET -> get radius config profile for android (secret_key is OTP)
group
=====
::
/v1/group/
GET -> list all group ids
POST -> create new group
/v1/group/{id}
GET -> get this group id
PUT -> overwrite group content
PATCH -> update via diff
DELETE -> whole entry
/v1/group/{id}/_attr/{attr}
GET -> get this groups attr
PUT -> overwrite this group attr value list
POST -> append this list to group attr
DELETE -> purge this attr
schema
======
Schema defines how we structure and store attributes, so we need a way to query
this and see what it contains.
::
/v1/schema/
GET -> list all class and attr types
::
/v1/schema/classtype/
GET -> list schema class names
POST -> create new class
/v1/schema/classtype/{id}
GET -> list schema class
PUT -> overwrite schema content
PATCH -> update via diff
/v1/schema/classtype/{id}/_attr/{attr}
GET -> list value of attr
PUT -> overwrite attr value
POST -> append list of values to attr
DELETE -> purge attr
::
/v1/schema/attributetype/
GET -> list schema class names
POST -> create new class
/v1/schema/attributetype/{id}
GET -> list schema class
PUT -> overwrite schema content
PATCH -> update via diff
/v1/schema/attributetype/{id}/_attr/{attr}
GET -> list value of attr
PUT -> overwrite attr value
POST -> append list of values to attr
DELETE -> purge attr
claims
======
TBD
recycle_bin
===========
List and restore from the recycle bin if possible.
::
/v1/recycle_bin/
GET -> list
/v1/recycle_bin/{id}
GET -> view recycled type
/v1/recycle_bin/{id}/_restore
POST -> restore this id.
access_profile
==============
::
/v1/access_profiles
GET -> list
POST -> create new acp
/v1/access_profiles/{id}
GET -> display acp
PUT -> overwrite acp
PATCH -> update via diff
DELETE -> delete this acp
/v1/access_profiles/{id}/_attr
GET -> list value of attr
PUT -> overwrite attr value
POST -> append list of values to attr
DELETE -> purge attr
References
==========
Great resource on api design
https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design
Has a great section on filtering strings that we should implement
https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md
Azure AD api as inspiration.
https://docs.microsoft.com/en-au/previous-versions/azure/ad/graph/api/functions-and-actions#changePassword
https://docs.microsoft.com/en-au/previous-versions/azure/ad/graph/api/users-operations
https://docs.microsoft.com/en-au/previous-versions/azure/ad/graph/api/groups-operations
https://github.com/mozilla-services/fernet-rs/blob/master/src/lib.rs
Other Notes
===========
What about a sudo/temporal claim assignment for pw change instead?
-- temporal claim that requires re-auth to add?
-- similar for self-write?
claims:
- enforce cred policy
- may not always be granted
- need a reauth+claim request interface
- claims must be able to be scoped by time
- uat signed/tamper proof
- similar when bearer.
- pw reset links must expire
- url should be a bearer signed containing expiry
- similar for radius profile view, should have a limited time scope on url.

View file

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ConsentText</key>
<dict>
<key>default</key>
<string>Consent Message</string>
</dict>
<key>PayloadContent</key>
<array>
<dict>
<key>AutoJoin</key>
<true/>
<key>CaptiveBypass</key>
<true/>
<key>EAPClientConfiguration</key>
<dict>
<key>AcceptEAPTypes</key>
<array>
<integer>25</integer>
</array>
<key>OuterIdentity</key>
<string>outerident</string>
<key>PayloadCertificateAnchorUUID</key>
<array>
<string>3507F18D-291F-4C03-885A-F3A33CD3A811</string>
</array>
<key>TLSMaximumVersion</key>
<string>1.2</string>
<key>TLSMinimumVersion</key>
<string>1.0</string>
<key>TLSTrustedServerNames</key>
<array/>
<key>UserName</key>
<string>username</string>
<key>UserPassword</key>
<string>password</string>
</dict>
<key>EncryptionType</key>
<string>WPA2</string>
<key>HIDDEN_NETWORK</key>
<false/>
<key>IsHotspot</key>
<false/>
<key>PayloadDescription</key>
<string>Configures Wi-Fi settings</string>
<key>PayloadDisplayName</key>
<string>Wi-Fi</string>
<key>PayloadIdentifier</key>
<string>com.apple.wifi.managed.74AC9A39-1A8F-48D8-A731-5C7D0491365A</string>
<key>PayloadType</key>
<string>com.apple.wifi.managed</string>
<key>PayloadUUID</key>
<string>74AC9A39-1A8F-48D8-A731-5C7D0491365A</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>ProxyType</key>
<string>None</string>
<key>SSID_STR</key>
<string>Blackhats</string>
</dict>
<dict>
<key>PayloadCertificateFileName</key>
<string>radius-ca.crt</string>
<key>PayloadContent</key>
<data>
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZjVENDQTFt
Z0F3SUJBZ0lGQUs2WXZhTXdEUVlKS29aSWh2Y05BUUVMQlFBd1hq
RUxNQWtHQTFVRUJoTUMKUVZVeEV6QVJCZ05WQkFnVENsRjFaV1Z1
YzJ4aGJtUXhFVEFQQmdOVkJBY1RDRUp5YVhOaVlXNWxNUkl3RUFZ
RApWUVFLRXdsQ2JHRmphMmhoZEhNeEV6QVJCZ05WQkFNVENtSm9J
R3hrWVhBZ1kyRXdIaGNOTVRnd09UQXhNREl4Ck9ETTRXaGNOTWpB
d09UQXhNREl4T0RNNFdqQmVNUXN3Q1FZRFZRUUdFd0pCVlRFVE1C
RUdBMVVFQ0JNS1VYVmwKWlc1emJHRnVaREVSTUE4R0ExVUVCeE1J
UW5KcGMySmhibVV4RWpBUUJnTlZCQW9UQ1VKc1lXTnJhR0YwY3pF
VApNQkVHQTFVRUF4TUtZbWdnYkdSaGNDQmpZVENDQWlJd0RRWUpL
b1pJaHZjTkFRRUJCUUFEZ2dJUEFEQ0NBZ29DCmdnSUJBTzhYdm1S
YmtkNm5BYlVUZi9KYzd6MWd6ampBbW1pTTJteFdLckl2ZE5DMGY3
WW94akdWMFhPK3hFRkQKWGMyaHZVY0JTaTd5cEVaaEVhS3NjSU9M
MGVlTEh6U2Nnanh0ZjBiOUZjaXhUbEhVd2FyM05sS0FIUHdKQnpH
UApmOXZNTGQ4dkdFaG8xd09JYkRSc0MzYitiOGg4SVJPNWVtSTlB
WTdqbzJlR3IvT2l3UFhPM2l3R29mcHdtZWFNCnhoa0o0NGNHMm9O
c2JiUjN0bkFMdzVDWm44RXlxbkNOSytERUQvUmhrWmJNSThXbU9N
dllxTFJJejJIOEM2bVoKaGdFSjNRWEpyRFJNYUxwVE41Vm1zRnFF
bnJiWnBadXAyQmFraDAwaXZBblJqNUNRdnBYZXIwQU9weHB5ZXlZ
UQpYNWJXalhVMjM3czE1SVhkeEs5SEhuMHRsVDFoamQ3cUNqWEN6
eEtxZy9QR2VwU2hZd1F6dzlwUTNmQ20yMzN2CnFYek0zeGQvVW9V
aVdrbkRCK3k2VVpBU3hRdHZETkF1T2QyV0lqUDA1SzRaaHFxSmdD
eWFycDJmbFNMb3IzSGUKVVhVWnByWTh1ckN1L2F4TE5uL1ZEcHFr
d0Rsek95cno2OEl3TEpIeE5CdzVWakltY1V2dEtEQjNMdDJLN0Ft
MwpvdHVEbVByMEw1MTlqeFBsZTlqOHUyMCtsakwzUW01d1FnQTBU
ZWwyZlZDUlF6OVgvNVlOVHBiVE55MXZHK3A3CjZ3Vi9jRUk5b2J2
cE5vUjZWck4ySjRSZE01dzN2NjlQQ0g5aURPZnRrenYrMWlNVE84
YWd3OVJrY0F4ZmtzWXYKWHdwUHZhQkR1eEk0NlFNUGpvRk43MXgv
d3AveHN3bmE2UjVUKyt3NGZIOG9qQVEvQWdNQkFBR2pOakEwTUFz
RwpBMVVkRHdRRUF3SUNCREFTQmdOVkhSTUJBZjhFQ0RBR0FRSC9B
Z0VBTUJFR0NXQ0dTQUdHK0VJQkFRUUVBd0lDCkJEQU5CZ2txaGtp
Rzl3MEJBUXNGQUFPQ0FnRUEwUHBtMldWWkJiOXRXUm8yZk9udk52
NStBNXN2NUltaUJMOEIKYjgxeUdFN2hXZHBQdnlkSGlFSXNqbjJz
U0t5L1FXTlBtZStGb2RqaEVsMVNyYlg1ZkwxMjdOQmpKVmMxYzZD
NgpPVmZOOEpXUVZlOHcvKzFJSTRMRmw2dmlxdEd1K0RBdmJHMWVH
bmVwVEZ4VjBUb25lazA3T3YreGM0TGJ3T29xCmJvYTMySG05alRM
OE95REkrUFUvNmZnZFc5b1EyK1ZCSDNJQkhYV1pudHhWV3R6TDJt
c3dJTzcvcDdDazdzRDQKem9kSnRzMkZOeXdHdjlXT3BDaEIrRGd0
Zks2dXVySzR2Kzd6UDBpRU9jWSt3V0Q1bEx0Z0I2RGJRVGRHQU1o
MApVNG5DVVRITS9SN3ZxZE5lT093Zlc2YllBVkhJMzluUnN3RWg0
S3FqWHBITFdDTWJ0Wmw2NU9idEZacCtKS25GCndGWm1FbWk2NjlX
bm1PWkNDMmlrUWg2TXNJd3RhSlFzKzZhTFA1UWptYythTmtYcDM2
OVNRK0V1SFpUQk0vMjkKczl5cWVkaHN2QkhiTWFjSG45WnI2QzEy
LytDSm5FeWdiSDFZQlFMRGQzL1ZNMlZZYTZyL2VEUkxjZ0pBa2M5
VApMeHZtYzJjWEhWUVVQMDNyT3N1UFNMUUt6a0hJUlZlQktHNlFZ
RFBlVmRxL1VsOHVDZXcxbFAwb2tLOGJ1M2VECk5iM0YrMUlRWW1T
UUhhaVdtVzYzYXZyWENNS01sMkhCOU9NWjNNdnlQemN5dGNGdHNR
cUVjcGRaQlNRaTZyMWcKVEdLTGVEQ1psRUVYa1NKaktmU2VMN3lz
Z21CT2xBbXp6VmlhS1VJbTQ3T3pZUUhGWFNxb2ZoVzlHdEtJc3hx
aQo5VHdVaE9jPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
</data>
<key>PayloadDescription</key>
<string>Adds a CA root certificate</string>
<key>PayloadDisplayName</key>
<string>bh ldap ca</string>
<key>PayloadIdentifier</key>
<string>com.apple.security.root.3507F18D-291F-4C03-885A-F3A33CD3A811</string>
<key>PayloadType</key>
<string>com.apple.security.root</string>
<key>PayloadUUID</key>
<string>3507F18D-291F-4C03-885A-F3A33CD3A811</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadDescription</key>
<string>Autogenerated Wifi Profile for Radius</string>
<key>PayloadDisplayName</key>
<string>wifi-blackhats</string>
<key>PayloadIdentifier</key>
<string>kanidm.1CAAD07A-89C7-4A3B-9AAD-C7C1DE7F69AE</string>
<key>PayloadOrganization</key>
<string>blackhats.net.au</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>E3A2F3F5-88E9-40B9-985C-1B35BD5314B3</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>

View file

@ -4,6 +4,7 @@ use actix_web::middleware::session::{self, RequestSession};
use actix_web::{
error, http, middleware, App, Error, HttpMessage, HttpRequest, HttpResponse, Result, State,
};
// use actix_web::Path;
use bytes::BytesMut;
use futures::{future, Future, Stream};
@ -274,6 +275,29 @@ fn idm_account_set_password(
)
}
/*
fn test_resource(
(class, _req, _state): (Path<String>, HttpRequest<AppState> ,State<AppState>),
) -> String {
format!("Hello {:?}!", class)
}
// https://actix.rs/docs/extractors/
#[derive(Deserialize)]
struct RestResource {
class: String,
id: String,
}
fn test_resource_id(
(r, _req, _state): (Path<RestResource>, HttpRequest<AppState> ,State<AppState>),
) -> String {
format!("Hello {:?}/{:?}!", r.class, r.id)
}
*/
// === internal setup helpers
fn setup_backend(config: &Configuration) -> Result<Backend, OperationError> {
let mut audit_be = AuditScope::new("backend_setup");
let pool_size: u32 = config.threads as u32;
@ -624,17 +648,9 @@ pub fn create_server_core(config: Configuration) {
// This forces https only if true
.secure(secure_cookies),
))
// .resource("/", |r| r.f(index))
.resource("/v1/whoami", |r| {
r.method(http::Method::GET).with_async(whoami)
})
// .resource("/v1/login", ...)
// .resource("/v1/logout", ...)
// .resource("/v1/token", ...) generate a token for id servers to use
// on clients, IE linux machines. Workflow being login -> token
// containing group uuids and information needed, as well as a
// set of data for user stuff
// curl --header "Content-Type: application/json" --request POST --data '{ "entries": [ {"attrs": {"class": ["group"], "name": ["testgroup"], "description": ["testperson"]}}]}' http://127.0.0.1:8080/v1/create
.resource("/v1/create", |r| {
r.method(http::Method::POST).with_async(create)
})
@ -655,6 +671,15 @@ pub fn create_server_core(config: Configuration) {
r.method(http::Method::POST)
.with_async(idm_account_set_password)
})
// Test resources
/*
.resource("/v1/account", |r| r.f(|_| "Hello Account"))
.resource("/v1/{class}/{id}",
|r| r.method(http::Method::GET).with(test_resource_id))
.resource("/v1/{class}",
|r| r.method(http::Method::GET).with(test_resource))
*/
// Add an ldap compat search function type?
/*

View file

@ -105,7 +105,7 @@ fn main() {
// Configure the server logger. This could be adjusted based on what config
// says.
if opt.debug() {
::std::env::set_var("RUST_LOG", "actix_web=info,kanidm=debug");
::std::env::set_var("RUST_LOG", "actix_web=debug,kanidm=debug");
} else {
::std::env::set_var("RUST_LOG", "actix_web=info,kanidm=info");
}