Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ Implements the Microsoft Office365 Graph API for the follow
- List channel messages
- Get channel message
- Send channel message
* Application
- List Applications
- Get Application
- Create Application
- Update Application
- Delete Application
- Application Add Password
- Application Add/Get Service Principal
- Application API Permissions grant Oauth2AdminGrant
- Application API Permissions grant admin consent


## Installation
Expand Down
2 changes: 1 addition & 1 deletion shard.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: office365
version: 1.25.6
version: 1.25.7

crystal: ">= 0.36.1"

Expand Down
42 changes: 42 additions & 0 deletions spec/applications/applications_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require "../spec_helper"

describe Office365::Application do
it "List applications" do
SpecHelper.mock_client_auth
SpecHelper.mock_list_applications

client = Office365::Client.new(**SpecHelper.mock_credentials)
list = client.list_applications
list.size.should eq(1)
end

it "creates application" do
SpecHelper.mock_client_auth
SpecHelper.mock_create_applications

client = Office365::Client.new(**SpecHelper.mock_credentials)
app = Office365::Application.new(display_name: "Display Name")
created_app = client.create_application(app)
created_app.should_not be_nil
created_app.sign_in_audience.should eq(Office365::SignInAud::AzureADandPersonalMicrosoftAccount)
end

it "add password to existing application" do
SpecHelper.mock_client_auth
SpecHelper.mock_applications_add_pwd

client = Office365::Client.new(**SpecHelper.mock_credentials)
pwd = client.application_add_pwd("my-app", "Password friendly name")
pwd.should_not be_nil
pwd.display_name.should eq("Password friendly name")
end

it "get partial application contents" do
SpecHelper.mock_client_auth
SpecHelper.mock_get_application_id_and_web
client = Office365::Client.new(**SpecHelper.mock_credentials)
app = client.get_application("my-app", "id,web")
app.web.should_not be_nil,
app.web.not_nil!.redirect_uris.try &.size.should eq(2)
end
end
4 changes: 2 additions & 2 deletions spec/chat_messages/channel_messages_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ describe Office365::Places do
channel_id = "19:4a95f7d8db4c4e7fae857bcebe0623e6@thread.tacv2"
client = Office365::Client.new(**SpecHelper.mock_credentials)

resp = client.send_channel_message(team_id, channel_id, "Hello World")
resp.status_code.should eq(201)
resp = client.send_channel_message(team_id, channel_id, "test")
resp.body.content.should eq("test")
end
end
end
147 changes: 145 additions & 2 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
Expand Up @@ -710,8 +710,151 @@ module SpecHelper

def mock_channel_send_msg
WebMock.stub(:post, "https://graph.microsoft.com/v1.0/teams/fbe2bf47-16c8-47cf-b4a5-4b9b187c508b/channels/19%3A4a95f7d8db4c4e7fae857bcebe0623e6%40thread.tacv2/messages")
.with(body: "{\"body\":{\"content\":\"Hello World\",\"contentType\":\"TEXT\"}}", headers: {"Authorization" => "Bearer access_token", "Content-Type" => "application/json", "Prefer" => "IdType=\"ImmutableId\""})
.to_return(status: 201, body: "")
.with(body: "{\"body\":{\"content\":\"test\",\"contentType\":\"TEXT\"}}", headers: {"Authorization" => "Bearer access_token", "Content-Type" => "application/json", "Prefer" => "IdType=\"ImmutableId\""})
.to_return(status: 201, body: mock_get_channel_message)
end

def mock_list_applications
WebMock.stub(:get, "https://graph.microsoft.com/v1.0/applications")
.to_return(body: mock_list_applications_resp)
end

def mock_create_applications
WebMock.stub(:post, "https://graph.microsoft.com/v1.0/applications")
.to_return(body: mock_create_application_resp)
end

def mock_applications_add_pwd
WebMock.stub(:post, "https://graph.microsoft.com/v1.0/applications%28appId%3D%27my-app%27%29/addPassword")
.with(body: "{\"passwordCredential\":{\"displayName\":\"Password friendly name\"}}", headers: {"Authorization" => "Bearer access_token", "Content-Type" => "application/json", "Prefer" => "IdType=\"ImmutableId\""})
.to_return(body: mock_application_add_pwd_resp)
end

def mock_get_application_id_and_web
WebMock.stub(:get, "https://graph.microsoft.com/v1.0/applications%28appId%3D%27my-app%27%29?%24select=id%2Cweb")
.to_return(body: mock_get_app_id_and_web_resp)
end

def mock_list_applications_resp
%(
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#applications",
"value": [
{
"appId": "00000000-0000-0000-0000-000000000000",
"identifierUris": [ "http://contoso/" ],
"displayName": "My app",
"publisherDomain": "contoso.com",
"signInAudience": "AzureADMyOrg"
}
]
}
)
end

def mock_create_application_resp
%(
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#applications/$entity",
"id": "03ef14b0-ca33-4840-8f4f-d6e91916010e",
"deletedDateTime": null,
"isFallbackPublicClient": null,
"appId": "631a96bc-a705-4eda-9f99-fdaf9f54f6a2",
"applicationTemplateId": null,
"identifierUris": [],
"createdDateTime": "2019-09-17T19:10:35.2742618Z",
"displayName": "Display name",
"isDeviceOnlyAuthSupported": null,
"groupMembershipClaims": null,
"optionalClaims": null,
"addIns": [],
"publisherDomain": "contoso.com",
"samlMetadataUrl": "https://graph.microsoft.com/2h5hjaj542de/app",
"signInAudience": "AzureADandPersonalMicrosoftAccount",
"tags": [],
"tokenEncryptionKeyId": null,
"api": {
"requestedAccessTokenVersion": 2,
"acceptMappedClaims": null,
"knownClientApplications": [],
"oauth2PermissionScopes": [],
"preAuthorizedApplications": []
},
"appRoles": [],
"publicClient": {
"redirectUris": []
},
"info": {
"termsOfServiceUrl": null,
"supportUrl": null,
"privacyStatementUrl": null,
"marketingUrl": null,
"logoUrl": null
},
"keyCredentials": [],
"parentalControlSettings": {
"countriesBlockedForMinors": [],
"legalAgeGroupRule": "Allow"
},
"passwordCredentials": [],
"requiredResourceAccess": [],
"web": {
"redirectUris": [],
"homePageUrl": null,
"logoutUrl": null,
"implicitGrantSettings": {
"enableIdTokenIssuance": false,
"enableAccessTokenIssuance": false
}
}
}

)
end

def mock_application_add_pwd_resp
%(
{
"customKeyIdentifier": null,
"endDateTime": "2021-09-09T19:50:29.3086381Z",
"keyId": "f0b0b335-1d71-4883-8f98-567911bfdca6",
"startDateTime": "2019-09-09T19:50:29.3086381Z",
"secretText": "[6gyXA5S20@MN+WRXAJ]I-TO7g1:h2P8",
"hint": "[6g",
"displayName": "Password friendly name"
}
)
end

def mock_get_app_id_and_web_resp
%(
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#applications(id,web)/$entity",
"id": "870cf357-927e-4d71-9f81-6ca278227636",
"web": {
"homePageUrl": null,
"logoutUrl": "https://localhost/auth/logout",
"redirectUris": [
"https://example.com",
"https://mydomain.com/auth/login"
],
"implicitGrantSettings": {
"enableAccessTokenIssuance": false,
"enableIdTokenIssuance": false
},
"redirectUriSettings": [
{
"uri": "https:/localhost:8843",
"index": null
},
{
"uri": "https://localhost:8843/auth/login",
"index": null
}
]
}
}
)
end
end

Expand Down
133 changes: 133 additions & 0 deletions src/applications.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
module Office365::Applications
def list_applications(filter : String? = nil) : Array(Application)
params = URI::Params.new
params["$select"] = filter.to_s if filter
path = "/v1.0/applications"
response = graph_request(graph_http_request(request_method: "GET", path: path, query: params))
if response.success?
Array(Application).from_json(response.body, "value")
else
raise "error listing applications #{response.status} (#{response.status_code}\n#{response.body}"
end
end

def get_application(app_id : String, filter : String? = nil) : Application
params = URI::Params.new
params["$select"] = filter.to_s if filter
path = "/v1.0/applications(appId='#{app_id}')"
response = graph_request(graph_http_request(request_method: "GET", path: path, query: params))
if response.success?
Application.from_json(response.body)
else
raise "error getting application #{response.status} (#{response.status_code}\n#{response.body}"
end
end

def create_application(app : Application) : Application
request = graph_http_request(request_method: "POST", path: "/v1.0/applications", data: app.to_json)
response = graph_request(request)
if response.success?
Application.from_json(response.body)
else
raise "error creating application #{response.status} (#{response.status_code}\n#{response.body}"
end
end

def delete_application(app_id : String) : Nil
path = "/v1.0/applications(appId='#{app_id}')"
response = graph_request(graph_http_request(request_method: "DELETE", path: path))
raise "error deleting application #{response.status} (#{response.status_code}\n#{response.body}" unless response.success?
end

def update_application(app_id : String, body : String) : Nil
path = "/v1.0/applications(appId='#{app_id}')"
response = graph_request(graph_http_request(request_method: "PATCH", path: path, data: body))
raise "error patching application #{response.status} (#{response.status_code}\n#{response.body}" unless response.status_code == 204
end

def application_add_pwd(app_id : String, display_name : String, start_date_time : Time? = nil, end_date_time : Time? = nil)
path = "/v1.0/applications(appId='#{app_id}')/addPassword"
creds = AppPasswordCredential.new(display_name, start_date_time, end_date_time)
body = {"passwordCredential": creds}
request = graph_http_request(request_method: "POST", path: path, data: body.to_json)
response = graph_request(request)
if response.success?
AppPasswordCredential.from_json(response.body)
else
raise "error adding application password #{response.status} (#{response.status_code}\n#{response.body}"
end
end

def application_create_sp(app_id : String) : String
path = "/v1.0/servicePrincipals"
body = {"appId": app_id}
request = graph_http_request(request_method: "POST", path: path, data: body.to_json)
response = graph_request(request)
if response.success?
JSON.parse(response.body).as_h["id"].as_s
else
raise "error creating application service principal #{response.status} (#{response.status_code}\n#{response.body}"
end
end

def application_get_sp(app_id : String) : String
params = URI::Params.new
params["$filter"] = "appId eq '#{app_id}'"
path = "/v1.0/servicePrincipals"
response = graph_request(graph_http_request(request_method: "GET", path: path, query: params))
if response.success?
JSON.parse(response.body).as_h["value"].as_a.first.as_h["id"].as_s
else
raise "error getting application service principal #{response.status} (#{response.status_code}\n#{response.body}"
end
end

def application_upsert_sp(app_id : String) : String
application_get_sp(app_id) rescue application_create_sp(app_id)
end

def application_add_app_role_assignment(app_id : String, role_id : String) : Hash(String, JSON::Any)
app_sp_id = application_upsert_sp(app_id)
graph_resource_id = application_get_sp("00000003-0000-0000-c000-000000000000")
application_add_app_role_assignment(app_sp_id, graph_resource_id, role_id)
end

def application_add_app_role_assignment(app_sp_id : String, graph_sp_id : String, role_id : String) : Hash(String, JSON::Any)
body = {
"principalId": app_sp_id,
"resourceId": graph_sp_id,
"appRoleId": role_id,
}
path = "/v1.0/servicePrincipals/#{app_sp_id}/appRoleAssignments"
request = graph_http_request(request_method: "POST", path: path, data: body.to_json)
response = graph_request(request)
if response.success?
JSON.parse(response.body).as_h
else
raise "error creating application service app role assignment #{response.status} (#{response.status_code}\n#{response.body}"
end
end

def application_add_oauth2_permission_grant(app_id : String, scope : String) : Hash(String, JSON::Any)
app_sp_id = application_upsert_sp(app_id)
graph_resource_id = application_get_sp("00000003-0000-0000-c000-000000000000")
application_add_oauth2_permission_grant(app_sp_id, graph_resource_id, scope)
end

def application_add_oauth2_permission_grant(app_sp_id : String, graph_sp_id : String, scope : String) : Hash(String, JSON::Any)
body = {
"clientId": app_sp_id,
"consentType": "AllPrincipals",
"resourceId": graph_sp_id,
"scope": scope,
}
path = "/v1.0/oauth2PermissionGrants"
request = graph_http_request(request_method: "POST", path: path, data: body.to_json)
response = graph_request(request)
if response.success?
JSON.parse(response.body).as_h
else
raise "error granting admin consent to delegated permissions #{response.status} (#{response.status_code}\n#{response.body}"
end
end
end
3 changes: 3 additions & 0 deletions src/client.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require "./odata"
require "./password_credentials"
require "./places"
require "./channel_messages"
require "./applications"

module Office365
USERS_BASE = "/v1.0/users"
Expand All @@ -29,6 +30,7 @@ module Office365
include Office365::PasswordCredentials
include Office365::Places
include Office365::ChannelMessages
include Office365::Applications

LOGIN_URI = URI.parse("https://login.microsoftonline.com")
GRAPH_URI = URI.parse("https://graph.microsoft.com/")
Expand Down Expand Up @@ -166,6 +168,7 @@ module Office365
property http_body : String

def initialize(@http_status, @http_body, @message = nil)
super("#{@http_status} #{@message} reason: #{@http_body}")
end
end
end
Loading