-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(keycloak): add keycloak rest api
for use by authenticated users
- Loading branch information
Showing
2 changed files
with
158 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
require "placeos-driver" | ||
require "link-header" | ||
|
||
class Keycloak::RestAPI < PlaceOS::Driver | ||
# Discovery Information | ||
generic_name :Keycloak | ||
descriptive_name "Keycloak service" | ||
uri_base "https://keycloak.domain.com" | ||
|
||
description %(uses users OAuth2 tokens provided during SSO to access keycloak APIs) | ||
|
||
default_settings({ | ||
place_domain: "https://placeos.org.com", | ||
place_api_key: "requires users scope", | ||
realm: "realm-id", | ||
}) | ||
|
||
@realm : String = "" | ||
@api_key : String = "" | ||
@place_domain : String = "" | ||
|
||
def on_load | ||
on_update | ||
end | ||
|
||
def on_update | ||
@realm = setting(String, :realm) || "" | ||
@api_key = setting(String, :place_api_key) || "" | ||
@place_domain = setting(String, :place_domain) || "" | ||
end | ||
|
||
struct Role | ||
include JSON::Serializable | ||
include JSON::Serializable::Unmapped | ||
|
||
getter id : String? | ||
getter name : String? | ||
getter description : String? | ||
end | ||
|
||
struct UserDetails | ||
include JSON::Serializable | ||
include JSON::Serializable::Unmapped | ||
|
||
getter id : String? | ||
getter username : String? | ||
getter enabled : Bool? | ||
getter email : String? | ||
|
||
@[JSON::Field(key: "firstName")] | ||
getter first_name : String? | ||
|
||
@[JSON::Field(key: "lastName")] | ||
getter last_name : String? | ||
|
||
@[JSON::Field(key: "realmRoles")] | ||
getter realm_roles : Array(String)? | ||
|
||
@[JSON::Field(key: "clientRoles")] | ||
getter client_roles : Array(Role)? | ||
|
||
@[JSON::Field(key: "applicationRoles")] | ||
getter application_roles : Array(Role)? | ||
getter groups : Array(String)? | ||
end | ||
|
||
def users( | ||
search : String? = nil, | ||
email : String? = nil, | ||
enabled_users_only : Bool = true, | ||
all_pages : Bool = false | ||
) | ||
user_token = "Bearer #{get_token}" | ||
|
||
params = URI::Params.build do |form| | ||
form.add "search", search.to_s if search.presence | ||
form.add "email", email.to_s if email.presence | ||
form.add "enabled", enabled_users_only.to_s | ||
form.add "exact", (!!email.presence).to_s | ||
|
||
# yes it starts at index 1? | ||
# https://github.com/keycloak/keycloak-community/blob/main/design/rest-api-guideline.md#pagination | ||
form.add "first", "1" | ||
form.add "max", "100" | ||
end | ||
|
||
# Get the existing bookings from the API to check if there is space | ||
users = [] of UserDetails | ||
next_request = "/admin/realms/#{@realm}/users?#{params}" | ||
headers = HTTP::Headers{ | ||
"Accept" => "application/json", | ||
"Authorization" => user_token, | ||
} | ||
|
||
logger.debug { "requesting users, all pages: #{all_pages}" } | ||
page_count = 1 | ||
|
||
loop do | ||
response = get(next_request, headers: headers) | ||
raise "unexpected error: #{response.status_code} - #{response.body}" unless response.success? | ||
|
||
links = LinkHeader.new(response) | ||
next_request = links["next"]? | ||
|
||
new_users = Array(UserDetails).from_json response.body | ||
users.concat new_users | ||
break if !all_pages || next_request.nil? || new_users.empty? | ||
page_count += 1 | ||
end | ||
|
||
logger.debug { "users count: #{users.size}, pages: #{page_count}" } | ||
|
||
users | ||
end | ||
|
||
def get_token | ||
user_id = invoked_by_user_id | ||
raise "only supports requests directly from SSO users" unless user_id | ||
get_user_token user_id | ||
end | ||
|
||
@[Security(Level::Administrator)] | ||
def get_user_token(user_id : String) : String | ||
response = ::HTTP::Client.post("#{@place_domain}/api/engine/v2/users/#{user_id}/resource_token", headers: HTTP::Headers{ | ||
"X-API-Key" => @api_key, | ||
}) | ||
raise "failed to obtain a keycloak API key for user #{user_id}: #{response.status_code} - #{response.body}" unless response.success? | ||
JSON.parse(response.body)["token"].as_s | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
require "placeos-driver/spec" | ||
|
||
DriverSpecs.mock_driver "Keycloak::RestAPI" do | ||
settings({ | ||
# we grab the HTTP port that the spec is using | ||
place_domain: "http://127.0.0.1:#{__get_ports__[1]}", | ||
place_api_key: "key", | ||
realm: "keycloak", | ||
}) | ||
|
||
resp = exec(:get_token, user_id: "user1") | ||
request_path = "" | ||
|
||
# should send a HTTP to place API to obtain the token | ||
expect_http_request do |request, response| | ||
request_path = request.path | ||
headers = request.headers | ||
response.status_code = 403 unless headers["X-API-Key"]? == "key" | ||
response << %({ | ||
"token": "a-token", | ||
"expires": 123445 | ||
}) | ||
end | ||
|
||
# What the sms function should return | ||
resp.get.should eq("a-token") | ||
request_path.should eq "/api/engine/v2/users/user1/resource_token" | ||
end |