deft/notes/cisco_ft_securex_registration.org

653 lines
19 KiB
Org Mode
Raw Normal View History

:PROPERTIES:
:ID: 1208f09c-d37d-4e6b-9110-151f3c6b7d34
:END:
#+TITLE: Cisco FT SecureX Simplified Registration
#+Author: Yann Esposito
#+Date: [2021-12-07]
#+OPTIONS: prop:t
- tags :: [[id:299643a7-00e5-47fb-a987-3b9278e89da3][Auth]]
- ref :: https://github.com/advthreat/response/issues/821
- source :: https://github.com/advthreat/iroh/issues/6076
- dashboard :: https://github.com/advthreat/iroh/projects/32
* Table of Content :TOC_3:QUOTE:
#+BEGIN_QUOTE
- [[#functional-spec][Functional Spec]]
- [[#15-technical-plan][=15= Technical Plan]]
- [[#0-support-private-email-vs-public-emails][=0= Support private email vs public emails]]
- [[#canceled-support-allow-list-exceptions-for-some-cisco-user][CANCELED Support allow-list exceptions for some Cisco user.]]
- [[#1-support-search-admin-with-same-email-domain][=1= Support, search admin with same email domain]]
- [[#10-new-ui-sync][=10= New UI Sync]]
- [[#1-new-auth-apis][=1= New Auth APIs]]
- [[#1-new-api-jwt-middleware][=1= New API JWT middleware]]
- [[#5-new-iroh-auth-api-endpoints][=5= New IROH-Auth API endpoints]]
- [[#2-new-iroh-auth-api][=2= New IROH-Auth API]]
- [[#1-new-iroh-auth-login-process][=1= New IROH-Auth login process.]]
- [[#3-email-notification-of-org-request-accesses][=3= Email Notification of Org Request Accesses]]
- [[#notes][Notes]]
- [[#2-org-requests-crud-api-for-admins-of-the-orgs][=2= Org Requests CRUD API for Admins of the Orgs]]
- [[#list][List]]
- [[#read][Read]]
- [[#patch-the-org-access][Patch the Org Access]]
#+END_QUOTE
* Functional Spec
Response issues:
- https://github.com/advthreat/response/issues/821
Figma: https://www.figma.com/file/Bz3m25kpWXpdct7AnhmNsW/SXSO-Registeration?node-id=759%3A5926
* =15= Technical Plan
/Estimate: 15 rcd/
- rcd :: rcd stands for release cycle for 1 dev
** DONE =0= Support private email vs public emails
*DONE*
+/Estimate: 1 rcd after the list is provided./+
The solution is to use a blacklist of domains where any user could create
multiple email accounts pseudo-anonymously.
Details: https://github.com/advthreat/response/issues/979
*** CANCELED Support allow-list exceptions for some Cisco user.
:LOGBOOK:
- State "CANCELED" from "HOLD" [2022-01-17 Mon 10:57] \\
This only concern new account creation. User with gmail account could still login.
:END:
/Estimate: 1 rcd after the list is provided./
Typically we should allow some users with an email like =some-user+XXX@gmail.com=
** =1= Support, search admin with same email domain
/Estimate: 1 rcd./
Note: *Work in progress*
We should be able given an email from a user, to find all the orgs for
which at least one of its admin has a matching domain name.
1. Most efficient: add an invisible field =email-domain= to all users. This
should be lower-case, and we will need a migration.
Doing this we could have a faster match than using string related queries.
Problems, users can login in the same user, with the same public email with
different emails.
This should be rare.
2. Search via text match.
The algorithm should look a bit like:
#+begin_src clojure
;; only when this is an unknown user
;; so a single approval will prevent the user to see this page.
(let [user-email ,,,
domain (string/replace user-email #".*@" "")
users (matching-admins domain) ;; returns a potentially big list of admin users
indexed-orgs (group-by :org-id users)]
(vals indexed-orgs))
#+end_src
Once this list of orgs is found.
We should also check the list of pending or rejected OrgAccessRequest for this user in
order to prevent the user to request access multiple time.
** =10= New UI Sync
/Estimate: 11 rcd/
We should find a way to hand the UI work to the UI team.
Right now, the page are all generated in IROH.
To reach that ideally we should sync the source code as a jar in IROH.
In order to give the UI the ability to make a front-end application, we
should create news APIS that support a new UserIdentity-level JWT
*** =1= New Auth APIs
/Estimate: 1 rcd/
ref :: https://github.com/advthreat/iroh/issues/6242
Continue to use the =/code= route of iroh-auth.
But instead of returning a Session Token, it will return a UserIdentity Token.
This is a JWT with the important data from the IdP:
- idp-id
- user-identity-id
- user-email
- etc…
When the user is redirected to an HTML generated page, we should add the
=code= in the anchor parameter of the route so the UI will be able to use
that code to retrieve a UserIdentity Token.
We should derive the current mechanism with code, but use the login code store.
**** Tasks
- Create a new function to generate these new JWT using the "token-infos"
*** =1= New API JWT middleware
/Estimate: 1 rcd/
- ref :: https://github.com/advthreat/iroh/issues/6266
We need to change the configuration of the check JWT middleware to support
UserIdentity Token instead. And use this configuration for this new API.
The =user-identity-jwt= should contain enough data to retrieve:
- =idp-mapping=
- =user-email=
- all other metas, as user-name, user-nick, etc…
*** =5= New IROH-Auth API endpoints
/Estimate: 5 rcd/
- ref :: https://github.com/advthreat/iroh/issues/6268
**** =4= ~OrgAccessRequestService~
/Estimate: 4 rcd/
- ref :: https://github.com/advthreat/iroh/issues/6272
This new service should be mostly a CRUD service on top of =TKStore= with the
following schema:
#+begin_src clojure
(s/defschema OrgAccessRequestStatus
(s/enum :pending :accepted :rejected))
(s/defschema OrgAccessRequest
(st/merge
{:id UUID
:idp-mapping IdPMapping
:user-email s/Str
:org-id s/Str
:status OrgAccessRequestStatus
:created-at DateTime}
(st/optional-keys
{:user-name s/Str
:user-nick s/Str
:granted-role Role ;; the role granted if the request is accepted
:approver-id UserId
:approver-email UserEmail ;; email of the approver
:updated-at DateTime
})))
#+end_src
#+begin_src clojure
(defprotocol OrgAccessRequestService
"See iroh-auth.registration.org-access-request.schemas/ServiceFns for schemas."
:extend-via-metadata true
;; service function for the Admins logged in SecureX
;; User filtered CRUD+Search for REST API related methods
(search-org-access-requests-for-org
[this request-identity filter-map pagination-params]
"Search all OrgAccessRequest of the org of the user of the request-identity")
(get-org-access-request
[this request-identity org-access-request-id]
"Return the OrgAccessRequest for a user using the org-access-request-id")
(patch-org-access-request
[this request-identity org-access-request-id org-access-request-patch]
"Change the status of an OrgAccessRequest to grant/reject it.
Note user creation could be a side effect.")
;; For the New Registration Page (the user logged in via the IdP successfully)
(search-org-access-requests-for-user-identity
[this user-identity filter-map pagination-params]
"Search all OrgAccessRequest made by this user identity accross all orgs.
This should only be visible via the registration page on the new API accepting
user-identity JWT")
(create-org-access-request
[this user-identity org-id]
"Create a new OrgAccessRequest.")
(delete-org-access-request
[this user-identity org-access-request-id]
"Remove an org request access.")
;; Internal CRUD+Search
(raw-search-org-access-requests
[this filter-map pagination-params]
"Search all OrgAccessRequest grants")
(raw-get-org-access-request
[this org-access-request-id]
"Return the OrgAccessRequest grant")
(raw-patch-org-access-request
[this org-access-request-id org-access-request-patch]
"Update the status of an OrgAccessRequest."))
#+end_src
***** =1= search/get/patch
/Estimate: 1 rcd/
***** =3= PATCH
/Estimate: 3 rcd/
The org-access-request-patch should have the following schema:
#+begin_src clojure
{(s/optional-key :role) Role
:status (s/enum :accepted :rejected)}
#+end_src
If the role is not provided, the default value should be =user=.
If the old ~OrgAccessRequest~ is not ~:pending~ this should throw a 400.
If the status is set to =:accepted=:
1. create a new user for the org of the ~OrgAccessRequest~.
2. send an email to the user that requested the access
If the status is set to =:rejected=, then send an email to the user that
requested the access.
Note we should test that the user created that way could login correctly.
**** =1= ~UserIdentityService~
/Estimate: 1 rcd/
- ref :: https://github.com/advthreat/iroh/issues/6274
Create a new service dedicated to ~UserIdentities~.
#+begin_src clojure
(defprotocol UserIdentityService
"Service which handle UserIdentities (mostly CRUD operations). "
(create-user-identity [this user-params])
(get-user-identity [this user-id])
(update-user-identity [this user-params])
(update-login-date [this user-id])
(search-user-identities
[this filter-map pagination-params]
[this filter-map pagination-params stringmatch-query-params])
(delete-user-identity [this user-id]))
#+end_src
It should also contain a field containing a set of hidden org-ids that will
be the orgs not to display on the registration webpages.
#+begin_src clojure
(s/defschema UserIdentity
(ist/open-schema-any-keys
{:id s/Str
:email
:name
:nickname
:last-logged-in [,,,]
(s/optional-key :hidden-orgs) #{,,,}}))
#+end_src
*** =2= New IROH-Auth API
- ref :: https://github.com/advthreat/iroh/issues/6268
/Estimate: 4 rcd/
/Depends: IROH-Auth CRUD Service/
This new API should only work with UserIdentity JWT.
#+begin_src
GET /iroh/iroh-auth-ui/whoami ;; whoami at the User Identity level
POST /iroh/iroh-auth-ui/org-access/:org-id ;; request access to a matching org
POST /iroh/iroh-auth-ui/hide-org/:org-id ;; ability to hide an org-access
GET /iroh/iroh-auth-ui/matching-accounts ;; list all the matching accounts
GET /iroh/iroh-auth-ui/matching-orgs ;; list all the matching orgs
GET /iroh/iroh-auth-ui/pending-invites ;; list all the pending invites
GET /iroh/iroh-auth-ui/registration-view ;; helper to make a single http call
#+end_src
**** =whoami= Endpoint
- ref :: https://github.com/advthreat/iroh/issues/6266
Write an endpoint that would use the ~UserIdentity~ JWT and decode it.
Ideally should instead read the value in DB via the ~UserIdentityService~.
**** Create New Account
/Estimate: 2 rcd/
Create a new endpoint to create a new account (user + org):
#+begin_src http
GET /iroh/iroh-auth-ui/create-new-account
Accept: application/json
Content-Type: application/json
User-Agent: ob-http
Authorization: Bearer ${user-identity-jwt}
{"org-name":"Cisco",
"country":"US"}
#+end_src
#+begin_src clojure
(s/defschema NewAccount
(st/merge
{:org-name :- s/Str
:country :- (apply s/enum country-iso-codes)
(st/optional-keys
{:department :- s/Str
:street1 :- s/Str
:street2 :- s/Str
:postal-code :- s/Str
:city :- s/Str})}))
(POST "/create-new-account" []
:summary "Given a code and some org-settings create a new account (new org and new user)"
:description "This is an internal temporary route needed to select the user/org."
:body [{:keys [country
org-name
department
street1
street2
postal-code
city] :as new-account} NewAccount]
(let [address (iroh-core/assoc-some?
{:country-iso-code country}
:department department
:street1 street1
:street2 street2
:postal-code postal-code
:city city)
org-settings {:name org-name
:address address}]
(create-new-account org-settings)))
#+end_src
As we now have a session, we should take care about a few details:
- should we keep track of the =origin=?
YES this is a security risk to prevent an attack with a redirect to the
wrong endpoint. So the redirect should be handled by the backend.
- should we prevent a user identity to create multiple accounts?
I don't think so. Not in the first round at least.
It will probably be easy to add a =created-by= metas in the org, and prevent
duplicates (or put a maximal number of authorized enabled orgs)
**** List Matching Accounts - ref ::
https://github.com/advthreat/iroh/issues/6270 #+begin_src http GET
/iroh/iroh-auth-ui/matching-accounts
Accept: application/json
Content-Type: application/json
User-Agent: ob-http
Authorization: Bearer ${user-identity-jwt}
#+end_src
It should return a list of object with the following schema sorted by last
logged in time by the user:
#+begin_src clojure
{:user User
:org Org
:last-login-date DateTime
:relative-last-login s/Str
:org-created-at DateTime
:relative-org-created-at s/Str
:activated? s/Bool
:org-nb-users s/Int}
#+end_src
Mainly should return the data structure used in the current selection
account page using similar order and functionalities.
**** List Pending Invites
- ref :: https://github.com/advthreat/iroh/issues/6269
#+begin_src http
GET /iroh/iroh-auth-ui/pending-invites
#+end_src
Here is an example value:
#+begin_src clojure
[{:role "admin",
:org-id "org-1",
:expires-in-nb-days 7,
:status "pending",
:invitee-email "chuck@example.org",
:inviter-user-id "org-1-admin-1"}]
#+end_src
We already retrieve pending invite in the code. The work would be about
taking care of having a specialized method from either an existing or a new
TK service dedicated to this functionality.
Use this method to expose it via the API.
**** Request Org Access
- ref :: https://github.com/advthreat/iroh/issues/6273
We need to create another store with another Entity for access request to an Org.
#+begin_src clojure
(s/defschema OrgAccessRequest
(st/merge
{:id UUID
:idp-mapping IdPMapping
:user-email s/Str
:org-id s/Str
:status (s/enum :pending :accepted :rejected)
:created-at DateTime}
(st/optional-keys
{:user-name s/Str
:user-nick s/Str
:approver-id UserId
:approver-email UserEmail ;; email of the approver
:updated-at DateTime
})))
#+end_src
When a user request access to an organization.
We should create this object in DB.
#+begin_src
POST /iroh/iroh-auth-ui/org-access/:org-id ;; request access to a matching org
#+end_src
#+begin_src http
POST /iroh/iroh-auth-ui/org-access/:org-id
Accept: application/json
Content-Type: application/json
User-Agent: ob-http
Authorization: Bearer ${user-identity-jwt}
#+end_src
With this, and if every check matches:
1. There is a known active admin of this org with an email with the same domain name
2. The org is active
We should:
1. create a new =OrgAccessRequest= object in DB.
2. Send emails (see the [[*[3] Email Notification of Org Request Accesses]] section)
**** List Matching Orgs
- ref :: https://github.com/advthreat/iroh/issues/6271
#+begin_src http
GET /iroh/iroh-auth-ui/matching-orgs
#+end_src
Given a ~UserIdentity~ with some email.
Should return every Org whose at least one admin has an email with the same
domain address.
So if you login via ~chuck@cisco.com~ you should return all orgs for which
there is at least one admin with a ~cisco.com~ email address.
Should return a list of objects with the following schema (sort to be defined):
#+begin_src clojure
{:org Org
:org-nb-users s/Int
:org-request-access-status OrgRequestAccessStatus}
#+end_src
*Note*
This endpoint is not straightforward.
We should list all current matching org, and concurrently list all current
=OrgRequestAccess= made by this =UserIdentity=.
Optionally we might also retrieve the orgs marked as hidden for this =UserIdentity= in DB.
We shall finally return a list of orgs with the current status and remove
the hidden ones.
We could probably imagine we want to support a query parameter to show
the hidden orgs in case a user want to cancel the hiding of some org.
**** Registration View
- ref :: https://github.com/advthreat/iroh/issues/6275
#+begin_src http
GET /iroh/iroh-auth-ui/registration-view
#+end_src
Should return the same result as the union of the calls to
=matching-accounts=, =matching-orgs= and =pending-invites=.
#+begin_src clojure
{:matching-accounts [,,,]
:matching-orgs [,,,]
:pending-invites [,,,]}
#+end_src
**** Hide Matching Org
- ref :: https://github.com/advthreat/iroh/issues/6276
#+begin_src http
POST /iroh/iroh-auth-ui/hide-org/:org-id
Accept: application/json
Content-Type: application/json
User-Agent: ob-http
Authorization: Bearer ${user-identity-jwt}
#+end_src
Should save in the ~UserIdentity~ store a new ~org-id~ to hide.
This impacts the call to ~matching-orgs~ such that orgs marked as hidden
should not be displayed when calling the ~matching-org~ route.
*** =1= New IROH-Auth login process.
/Estimate: 1 rcd/
Every-time a users successfully login via an IdP we should synchronize the
=UserIdentity= value in our DB accordingly.
It should occurs not only during the account registration, but also for
every new login.
** =3= Email Notification of Org Request Accesses
/Estimate: 3 rcd after email+html templates/
1. List all the admins of the requested org.
2.a. If there is fewer admins than a number that could be configured in the
node configuration. Then we send an email to all admins.
2.b. If there are more admins than this specific number, then we randomly
chose this maximal number of admins and send them an email notification.
*TODO* Have an email template (both HTML and Text)
*** Notes
Need to be able to trigger a new request to join after 7 days.
** =2= Org Requests CRUD API for Admins of the Orgs
/Estimate: 2 rcd after mail templates + text/
There should be a CRUD API restricted to the ~admin/user-mgmt/org-requests~ scope:
- ~GET /iroh/user-mgmt/org-requests~ list pending org access requests
- ~GET /iroh/user-mgmt/org-requests/<id>~ read a single org access request
- ~PATCH /iroh/user-mgmt/org-requests/<id>~ Grant or Reject the access
*** List
~GET /iroh/user-mgmt/org-requests~
If no parameter is provided, only list pending =OrgAccessRequests= of the org
of the caller.
Otherwise we could pass the query-parameter =status= with the following
value(s):
- =pending=
- =accepted=
- =rejected=
Note we should probably support duplicate statuses.
Ex:
~GET /iroh/user-mgmt/org-requests?status=accepted&status=pending~
*** Read
~GET /iroh/user-mgmt/org-requests/org-request-id~
Should returns a 404 if not found or the single Org Access Request object.
*** Patch the Org Access
~PATCH /iroh/user-mgmt/org-requests/<id>~ Grant/Reject the access
The body should be a JSON Object with:
- an optional field =role= whose value could be either =admin= or =user= (default
to =user=)
- a mandatory =status= field whose value could be either =accepted= or =rejected=.
A few examples of valid values:
#+begin_src js
{"granted-role":"admin"
"status":"accepted"}
#+end_src
#+begin_src js
{"status":"accepted"}
#+end_src
#+begin_src js
{"status":"rejected"}
#+end_src
During the call we should:
1. Create a new user with:
#+begin_src clojure
{:user-id (gen-uuid)
:org-id (:org-id org-access-request)
:user-email (:user-email org-access-request)
:idp-mappings [(:idp-mapping org-access-request)]
:user-name (:user-name org-access-request)
:user-nick (:user-nick org-access-request)
:role (get-in request [:body :role])
:enabled? true
}
#+end_src
2. Send an email to user confirming his access was granted.
*TODO* have an email template + text.