power-bi 2.2.0 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +61 -0
- data/lib/power-bi/profile.rb +44 -0
- data/lib/power-bi/tenant.rb +26 -1
- data/lib/power-bi.rb +2 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c120eeef2ac35b02afcfff32690f003efad7fbfd59c7f2bd714a6d0659b1ed44
|
4
|
+
data.tar.gz: ee077e4fa8f6faae52ad5c1107fd96f9cb8bae5e7c8f0ef47a83009e7c80ce0d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c9c742362808fb3fa08ad663ebbdaf34d916f932fa5bed8f66d657caf769c230a7694d82cd05555a642d41098ccb9fcf6eed9b6f89233ca344ff3f1c3d02b6fb
|
7
|
+
data.tar.gz: dc28a5e89d26365b7b48d5f6538bd1e8c12a9fe45c81b4f216ae63074ced3b728f31395c98751ee571a9e7234315693af5f6a7cffe4d86c398f6ec6683cfeafe
|
data/README.md
CHANGED
@@ -10,6 +10,60 @@ The Power BI API does not handle the authorization part. It requires the user to
|
|
10
10
|
pbi = PowerBI::Tenant.new(->{token = get_token(client, token) ; token.token})
|
11
11
|
```
|
12
12
|
|
13
|
+
## Authentication & authorization towards the Power BI API
|
14
|
+
|
15
|
+
Currently (april 2024), there are basically 3 ways to authenticate to the Power BI API.
|
16
|
+
|
17
|
+
### 1 - Master User
|
18
|
+
|
19
|
+
A master user is a classic Microsoft 365 user that you assign a Power BI Pro license. In order to allow the user to execute actions on the Power BI API you need to create an app registration in Azure AD. In the associated Enterprise application (gets created when you create the app registration), you need to add the permissions to use the Power BI service.
|
20
|
+
|
21
|
+
The resulting authentication looks like this:
|
22
|
+
|
23
|
+
```
|
24
|
+
TENANT = "53c835d6-6841-4d58-948a-55117409e1d8"
|
25
|
+
CLIENT_ID = '6fc64675-bee3-49a7-70d8-b3301a51a88d'
|
26
|
+
CLIENT_SECRET = 'sI/@ncYe=eVt7.XfZ7tsPU1aPbxm0V_H'
|
27
|
+
USERNAME = 'company_0001@example.com'
|
28
|
+
PASSWORD = 'mLXv5A1jrIb8dHopur7y'
|
29
|
+
|
30
|
+
client = OAuth2::Client.new(CLIENT_ID, CLIENT_SECRET, site: 'https://login.microsoftonline.com', token_url: "#{TENANT}/oauth2/token")
|
31
|
+
|
32
|
+
token = client.password.get_token(USERNAME, PASSWORD, scope: 'openid', resource: 'https://analysis.windows.net/powerbi/api')
|
33
|
+
```
|
34
|
+
|
35
|
+
Note that in this case the legacy (and slightly unsafe) Resource Owner Password Credentials (ROPC) OAuth flow is used.
|
36
|
+
|
37
|
+
### 2 - Service principal
|
38
|
+
|
39
|
+
A service principal is a fancy word for a machine user with a secret (eg. id + key) in Azure AD. You create them by creating an app registration and adding a secret on it. You need to allow service principals in your Power BI admin settings. But once you allow that, the setup is very easy. No need to configure anything special in AD.
|
40
|
+
|
41
|
+
The resulting authentication looks like this:
|
42
|
+
|
43
|
+
```
|
44
|
+
TENANT = "53c835d6-6841-4d58-948a-55117409e1d8"
|
45
|
+
CLIENT_ID = '6fc64675-bee3-49a7-70d8-b3301a51a88d'
|
46
|
+
CLIENT_SECRET = 'sI/@ncYe=eVt7.XfZ7tsPU1aPbxm0V_H'
|
47
|
+
|
48
|
+
client = OAuth2::Client.new(CLIENT_ID, CLIENT_SECRET, site: 'https://login.microsoftonline.com', token_url: "#{TENANT}/oauth2/token")
|
49
|
+
|
50
|
+
token = client.client_credentials.get_token(resource: 'https://analysis.windows.net/powerbi/api')
|
51
|
+
```
|
52
|
+
|
53
|
+
Note that in this case the Client Credentials OAuth flow is used.
|
54
|
+
|
55
|
+
### 3 - Service principal profiles
|
56
|
+
|
57
|
+
Service principal profiles is a Power-BI-only concept. Towards AD, it looks exactly the same as generic service principal. Hence the authenication looks exactly as in way 2.
|
58
|
+
|
59
|
+
Once authenticated, you can set profiles like this:
|
60
|
+
|
61
|
+
```
|
62
|
+
pbi.profile = profile
|
63
|
+
```
|
64
|
+
|
65
|
+
Every action executed after setting the profile, will be executed _through the eyes of the profile_. This way, you can create an isolated multi-tenant setup. Using profiles, simplifies the internal organization of Power BI and allows faster interaction with Power BI. This also lifts the 1000-workspaces limit that is imposed on Master Userss and Service Principals
|
66
|
+
|
13
67
|
# Supported endpoints
|
14
68
|
|
15
69
|
Note: where possible we use _lazy evaluation_: we only call the REST API endpoint when really needed. For examples `pbi.workspaces` won't trigger a call, while `pbi.workspaces.count` will trigger a call. And `pbi.workspace('123')` won't trigger a call, while `pbi.workspace('123').name` will trigger a call.
|
@@ -75,6 +129,13 @@ Note: Capacities are Azure creatures, you can't create them in Power BI.
|
|
75
129
|
* List capacities: `pbi.capacities`
|
76
130
|
* Get a capacity: `pbi.capacity(id)`
|
77
131
|
|
132
|
+
## Profiles
|
133
|
+
|
134
|
+
* List profiles: `pbi.profiles`
|
135
|
+
* Get a profile: `pbi.profile(id)`
|
136
|
+
* Create a profile: `pbi.profiles.create`
|
137
|
+
* Delete a profile: `profile.delete`
|
138
|
+
|
78
139
|
# Note about gateway credentials
|
79
140
|
|
80
141
|
Power BI uses an obscure mechanism to encrypt credential exchange between the service and the gateway. The encryption must be done outside this module on a Windows machine based on th public key of the gateway. This is an example C# script:
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module PowerBI
|
2
|
+
class Profile < Object
|
3
|
+
|
4
|
+
def initialize(tenant, parent, id = nil)
|
5
|
+
super(tenant, id)
|
6
|
+
end
|
7
|
+
|
8
|
+
def get_data(id)
|
9
|
+
@tenant.get("/profiles/#{id}")
|
10
|
+
end
|
11
|
+
|
12
|
+
def data_to_attributes(data)
|
13
|
+
{
|
14
|
+
id: data[:id],
|
15
|
+
display_name: data[:displayName],
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete
|
20
|
+
@tenant.delete("/profiles/#{@id}")
|
21
|
+
@tenant.profiles.reload
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
class ProfileArray < Array
|
28
|
+
def self.get_class
|
29
|
+
Profile
|
30
|
+
end
|
31
|
+
|
32
|
+
def create(name)
|
33
|
+
data = @tenant.post("/profiles") do |req|
|
34
|
+
req.body = {displayName: name}.to_json
|
35
|
+
end
|
36
|
+
self.reload
|
37
|
+
Profile.instantiate_from_data(@tenant, nil, data)
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_data
|
41
|
+
@tenant.get("/profiles")[:value]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/power-bi/tenant.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
module PowerBI
|
2
2
|
class Tenant
|
3
|
-
attr_reader :workspaces, :gateways, :capacities
|
3
|
+
attr_reader :workspaces, :gateways, :capacities, :profiles, :profile
|
4
4
|
|
5
5
|
def initialize(token_generator, retries: 5, logger: nil)
|
6
6
|
@token_generator = token_generator
|
7
7
|
@workspaces = WorkspaceArray.new(self)
|
8
8
|
@gateways = GatewayArray.new(self)
|
9
9
|
@capacities = CapacityArray.new(self)
|
10
|
+
@profiles = ProfileArray.new(self)
|
10
11
|
@logger = logger
|
12
|
+
@profile = nil
|
11
13
|
|
12
14
|
## WHY RETRIES? ##
|
13
15
|
# It is noticed that once in a while (~0.1% API calls), the Power BI server returns a 500 (internal server error) without apparent reason, just retrying works :-)
|
@@ -42,6 +44,11 @@ module PowerBI
|
|
42
44
|
Capacity.new(self, nil, id)
|
43
45
|
end
|
44
46
|
|
47
|
+
def profile=(profile)
|
48
|
+
@profile = profile
|
49
|
+
@workspaces.reload # we need to reload the workspaces because we look through the eyes of the profile
|
50
|
+
end
|
51
|
+
|
45
52
|
def get(url, params = {})
|
46
53
|
t0 = Time.now
|
47
54
|
conn = Faraday.new do |f|
|
@@ -51,6 +58,9 @@ module PowerBI
|
|
51
58
|
req.params = params
|
52
59
|
req.headers['Accept'] = 'application/json'
|
53
60
|
req.headers['authorization'] = "Bearer #{token}"
|
61
|
+
if @profile
|
62
|
+
req.headers['X-PowerBI-Profile-Id'] = @profile.id
|
63
|
+
end
|
54
64
|
yield req if block_given?
|
55
65
|
end
|
56
66
|
if response.status == 400
|
@@ -73,6 +83,9 @@ module PowerBI
|
|
73
83
|
response = conn.get(PowerBI::BASE_URL + url) do |req|
|
74
84
|
req.params = params
|
75
85
|
req.headers['authorization'] = "Bearer #{token}"
|
86
|
+
if @profile
|
87
|
+
req.headers['X-PowerBI-Profile-Id'] = @profile.id
|
88
|
+
end
|
76
89
|
yield req if block_given?
|
77
90
|
end
|
78
91
|
log "Calling (GET - raw) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms"
|
@@ -92,6 +105,9 @@ module PowerBI
|
|
92
105
|
req.headers['Accept'] = 'application/json'
|
93
106
|
req.headers['Content-Type'] = 'application/json'
|
94
107
|
req.headers['authorization'] = "Bearer #{token}"
|
108
|
+
if @profile
|
109
|
+
req.headers['X-PowerBI-Profile-Id'] = @profile.id
|
110
|
+
end
|
95
111
|
yield req if block_given?
|
96
112
|
end
|
97
113
|
log "Calling (POST) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms"
|
@@ -113,6 +129,9 @@ module PowerBI
|
|
113
129
|
req.headers['Accept'] = 'application/json'
|
114
130
|
req.headers['Content-Type'] = 'application/json'
|
115
131
|
req.headers['authorization'] = "Bearer #{token}"
|
132
|
+
if @profile
|
133
|
+
req.headers['X-PowerBI-Profile-Id'] = @profile.id
|
134
|
+
end
|
116
135
|
yield req if block_given?
|
117
136
|
end
|
118
137
|
log "Calling (PATCH) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms"
|
@@ -133,6 +152,9 @@ module PowerBI
|
|
133
152
|
req.params = params
|
134
153
|
req.headers['Accept'] = 'application/json'
|
135
154
|
req.headers['authorization'] = "Bearer #{token}"
|
155
|
+
if @profile
|
156
|
+
req.headers['X-PowerBI-Profile-Id'] = @profile.id
|
157
|
+
end
|
136
158
|
yield req if block_given?
|
137
159
|
end
|
138
160
|
log "Calling (DELETE) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms"
|
@@ -158,6 +180,9 @@ module PowerBI
|
|
158
180
|
req.headers['Accept'] = 'application/json'
|
159
181
|
req.headers['Content-Type'] = 'multipart/form-data'
|
160
182
|
req.headers['authorization'] = "Bearer #{token}"
|
183
|
+
if @profile
|
184
|
+
req.headers['X-PowerBI-Profile-Id'] = @profile.id
|
185
|
+
end
|
161
186
|
req.body = {value: Faraday::UploadIO.new(file, 'application/octet-stream')}
|
162
187
|
req.options.timeout = 120 # default is 60 seconds Net::ReadTimeout
|
163
188
|
end
|
data/lib/power-bi.rb
CHANGED
@@ -24,4 +24,5 @@ require_relative "power-bi/gateway"
|
|
24
24
|
require_relative "power-bi/gateway_datasource"
|
25
25
|
require_relative "power-bi/page"
|
26
26
|
require_relative "power-bi/user"
|
27
|
-
require_relative "power-bi/capacity"
|
27
|
+
require_relative "power-bi/capacity"
|
28
|
+
require_relative "power-bi/profile"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: power-bi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lode Cools
|
@@ -98,6 +98,7 @@ files:
|
|
98
98
|
- lib/power-bi/object.rb
|
99
99
|
- lib/power-bi/page.rb
|
100
100
|
- lib/power-bi/parameter.rb
|
101
|
+
- lib/power-bi/profile.rb
|
101
102
|
- lib/power-bi/refresh.rb
|
102
103
|
- lib/power-bi/report.rb
|
103
104
|
- lib/power-bi/tenant.rb
|