power-bi 2.2.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5aaadec6cec57b22f4780dd2e1f4c64c55872d49f5a17a59326fcff099844dd
4
- data.tar.gz: 1a6bf8c48a1d17333f58519c5ee23ee9a85025c63110c6cc2f2fe9243566f7ca
3
+ metadata.gz: 90d415a89ed5860c82824f7e152e4c58d42054bc293f5534f6669774682a1ffe
4
+ data.tar.gz: 536d4c7cea48c859c7b780db751da15375b060a45c59946a2335887d8e948494
5
5
  SHA512:
6
- metadata.gz: 570af0ad49001778d237e4ce712a8120d879cc83d53a9b4a877d2d51ee6fcc3dde4e913cc8a3c42273ce5de45d31d7922308fee03c895fb6fcbce46332b06288
7
- data.tar.gz: 760813b41e3318fc52871f3adc61f4efcf518f9333f93fcf9295c15b9e61d094518cbd3adbbf7fec0c3c7921239d2082460099fc2adffcd4b60c15067e5946e1
6
+ metadata.gz: d31db71feb4b03d7810d5805b3d48016e89ead1782bfae66269aa2a0c55340e6cf21031525ffa6d5b917cdc36041670ef52c1ccff499ebdfc7d0051d8005b6a8
7
+ data.tar.gz: 1ea49ae1a15e45b16c21b9da86c8ac20abb9e96a6eb63e6a0ed9a7dfcbca25dfff8431896703f2d55736a148668cf4b85ac591eb20c68bf1b2d9f6baee455e3a
data/README.md CHANGED
@@ -10,6 +10,62 @@ 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 Users and Service Principals
66
+
67
+ Note: when working with Service principal profiles (SPP), you need to add the SPP to the gateway datasource before binding the gateway datasource to the dataset.
68
+
13
69
  # Supported endpoints
14
70
 
15
71
  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.
@@ -68,6 +124,11 @@ Note 2: to limit the number of API calls, it is best to directly use the _getter
68
124
  * Create a new gateway datasource: `gateway.gateway_datasource.create(name, credentials, db_server, db_name)`
69
125
  * Delete a new gateway datasource: `gateway_datasource.delete`
70
126
 
127
+ ## Gateway datasource users
128
+
129
+ * List datasource users in a gateway datasource: `gateway_datasource.gateway_datasource_users`
130
+ * Add a Service principal profile to a gateway datasource: `gateway_datasource.add_service_principal_profile_user(profile_id, principal_object_id)`
131
+
71
132
  ## Capacities
72
133
 
73
134
  Note: Capacities are Azure creatures, you can't create them in Power BI.
@@ -75,6 +136,13 @@ Note: Capacities are Azure creatures, you can't create them in Power BI.
75
136
  * List capacities: `pbi.capacities`
76
137
  * Get a capacity: `pbi.capacity(id)`
77
138
 
139
+ ## Profiles
140
+
141
+ * List profiles: `pbi.profiles`
142
+ * Get a profile: `pbi.profile(id)`
143
+ * Create a profile: `pbi.profiles.create`
144
+ * Delete a profile: `profile.delete`
145
+
78
146
  # Note about gateway credentials
79
147
 
80
148
  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:
@@ -8,7 +8,7 @@ module PowerBI
8
8
  end
9
9
 
10
10
  def get_data(id)
11
- @tenant.get("/gateways/#{id}")
11
+ @tenant.get("/gateways/#{id}", use_profile: false)
12
12
  end
13
13
 
14
14
  def data_to_attributes(data)
@@ -32,7 +32,7 @@ module PowerBI
32
32
  end
33
33
 
34
34
  def get_data
35
- @tenant.get("/gateways")[:value]
35
+ @tenant.get("/gateways", use_profile: false)[:value]
36
36
  end
37
37
  end
38
38
  end
@@ -1,14 +1,15 @@
1
1
  module PowerBI
2
2
  class GatewayDatasource < Object
3
- attr_reader :gateway
3
+ attr_reader :gateway, :gateway_datasource_users
4
4
 
5
5
  def initialize(tenant, parent, id = nil)
6
6
  super(tenant, id)
7
7
  @gateway = parent
8
+ @gateway_datasource_users = GatewayDatasourceUserArray.new(@tenant, self)
8
9
  end
9
10
 
10
11
  def get_data(id)
11
- @tenant.get("/gateways/#{@gateway.id}/datasources/#{id}")
12
+ @tenant.get("/gateways/#{@gateway.id}/datasources/#{id}", use_profile: false)
12
13
  end
13
14
 
14
15
  def data_to_attributes(data)
@@ -24,7 +25,7 @@ module PowerBI
24
25
  end
25
26
 
26
27
  def update_credentials(encrypted_credentials)
27
- @tenant.patch("/gateways/#{gateway.id}/datasources/#{id}") do |req|
28
+ @tenant.patch("/gateways/#{gateway.id}/datasources/#{id}", use_profile: false) do |req|
28
29
  req.body = {
29
30
  credentialDetails: {
30
31
  credentialType: "Basic",
@@ -41,7 +42,7 @@ module PowerBI
41
42
  end
42
43
 
43
44
  def delete
44
- @tenant.delete("/gateways/#{gateway.id}/datasources/#{id}")
45
+ @tenant.delete("/gateways/#{gateway.id}/datasources/#{id}", use_profile: false)
45
46
  @gateway.gateway_datasources.reload
46
47
  true
47
48
  end
@@ -61,7 +62,7 @@ module PowerBI
61
62
 
62
63
  # only MySQL type is currently supported
63
64
  def create(name, encrypted_credentials, db_server, db_name)
64
- data = @tenant.post("/gateways/#{@gateway.id}/datasources",) do |req|
65
+ data = @tenant.post("/gateways/#{@gateway.id}/datasources", use_profile: false) do |req|
65
66
  req.body = {
66
67
  connectionDetails: {server: db_server, database: db_name}.to_json,
67
68
  credentialDetails: {
@@ -82,7 +83,7 @@ module PowerBI
82
83
  end
83
84
 
84
85
  def get_data
85
- @tenant.get("/gateways/#{@gateway.id}/datasources")[:value]
86
+ @tenant.get("/gateways/#{@gateway.id}/datasources", use_profile: false)[:value]
86
87
  end
87
88
  end
88
89
  end
@@ -0,0 +1,51 @@
1
+ module PowerBI
2
+ class GatewayDatasourceUser < Object
3
+ attr_reader :gateway_datasource
4
+
5
+ def initialize(tenant, parent, id = nil)
6
+ super(tenant, id)
7
+ @gateway_datasource = parent
8
+ end
9
+
10
+ def data_to_attributes(data)
11
+ {
12
+ datasource_access_right: data[:datasourceAccessRight],
13
+ display_name: data[:displayName],
14
+ email_address: data[:emailAddress],
15
+ identifier: data[:identifier],
16
+ principal_type: data[:principalType],
17
+ profile: data[:profile],
18
+ }
19
+ end
20
+
21
+ end
22
+
23
+ class GatewayDatasourceUserArray < Array
24
+
25
+ def initialize(tenant, gateway_datasource)
26
+ super(tenant, gateway_datasource)
27
+ @gateway_datasource = gateway_datasource
28
+ end
29
+
30
+ def self.get_class
31
+ GatewayDatasourceUser
32
+ end
33
+
34
+ # service principal object ID: https://learn.microsoft.com/en-us/power-bi/developer/embedded/embedded-troubleshoot#what-is-the-difference-between-application-object-id-and-principal-object-id
35
+ def add_service_principal_profile_user(profile_id, principal_object_id, datasource_access_right: "Read")
36
+ @tenant.post("/gateways/#{@gateway_datasource.gateway.id}/datasources/#{@gateway_datasource.id}/users", use_profile: false) do |req|
37
+ req.body = {
38
+ datasourceAccessRight: datasource_access_right,
39
+ identifier: principal_object_id,
40
+ principalType: "App",
41
+ profile: {id: profile_id},
42
+ }.to_json
43
+ end
44
+ self.reload
45
+ end
46
+
47
+ def get_data
48
+ @tenant.get("/gateways/#{@gateway_datasource.gateway.id}/datasources/#{@gateway_datasource.id}/users", use_profile: false)[:value]
49
+ end
50
+ end
51
+ end
@@ -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}", use_profile: false)
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}", use_profile: false)
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", use_profile: false) 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", use_profile: false)[:value]
42
+ end
43
+ end
44
+ end
@@ -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_id
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_id = 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,7 +44,16 @@ module PowerBI
42
44
  Capacity.new(self, nil, id)
43
45
  end
44
46
 
45
- def get(url, params = {})
47
+ def profile(id)
48
+ Profile.new(self, nil, id)
49
+ end
50
+
51
+ def profile=(profile)
52
+ @profile_id = profile.is_a?(String) ? profile : profile&.id
53
+ @workspaces.reload # we need to reload the workspaces because we look through the eyes of the profile
54
+ end
55
+
56
+ def get(url, params = {}, use_profile: true)
46
57
  t0 = Time.now
47
58
  conn = Faraday.new do |f|
48
59
  f.request :retry, @retry_options
@@ -51,6 +62,9 @@ module PowerBI
51
62
  req.params = params
52
63
  req.headers['Accept'] = 'application/json'
53
64
  req.headers['authorization'] = "Bearer #{token}"
65
+ if use_profile
66
+ add_spp_header(req)
67
+ end
54
68
  yield req if block_given?
55
69
  end
56
70
  if response.status == 400
@@ -65,7 +79,7 @@ module PowerBI
65
79
  end
66
80
  end
67
81
 
68
- def get_raw(url, params = {})
82
+ def get_raw(url, params = {}, use_profile: true)
69
83
  t0 = Time.now
70
84
  conn = Faraday.new do |f|
71
85
  f.request :retry, @retry_options
@@ -73,6 +87,9 @@ module PowerBI
73
87
  response = conn.get(PowerBI::BASE_URL + url) do |req|
74
88
  req.params = params
75
89
  req.headers['authorization'] = "Bearer #{token}"
90
+ if use_profile
91
+ add_spp_header(req)
92
+ end
76
93
  yield req if block_given?
77
94
  end
78
95
  log "Calling (GET - raw) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms"
@@ -82,7 +99,7 @@ module PowerBI
82
99
  response.body
83
100
  end
84
101
 
85
- def post(url, params = {})
102
+ def post(url, params = {}, use_profile: true)
86
103
  t0 = Time.now
87
104
  conn = Faraday.new do |f|
88
105
  f.request :retry, @retry_options
@@ -92,6 +109,9 @@ module PowerBI
92
109
  req.headers['Accept'] = 'application/json'
93
110
  req.headers['Content-Type'] = 'application/json'
94
111
  req.headers['authorization'] = "Bearer #{token}"
112
+ if use_profile
113
+ add_spp_header(req)
114
+ end
95
115
  yield req if block_given?
96
116
  end
97
117
  log "Calling (POST) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms"
@@ -103,7 +123,7 @@ module PowerBI
103
123
  end
104
124
  end
105
125
 
106
- def patch(url, params = {})
126
+ def patch(url, params = {}, use_profile: true)
107
127
  t0 = Time.now
108
128
  conn = Faraday.new do |f|
109
129
  f.request :retry, @retry_options
@@ -113,6 +133,9 @@ module PowerBI
113
133
  req.headers['Accept'] = 'application/json'
114
134
  req.headers['Content-Type'] = 'application/json'
115
135
  req.headers['authorization'] = "Bearer #{token}"
136
+ if use_profile
137
+ add_spp_header(req)
138
+ end
116
139
  yield req if block_given?
117
140
  end
118
141
  log "Calling (PATCH) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms"
@@ -124,7 +147,7 @@ module PowerBI
124
147
  end
125
148
  end
126
149
 
127
- def delete(url, params = {})
150
+ def delete(url, params = {}, use_profile: true)
128
151
  t0 = Time.now
129
152
  conn = Faraday.new do |f|
130
153
  f.request :retry, @retry_options
@@ -133,6 +156,9 @@ module PowerBI
133
156
  req.params = params
134
157
  req.headers['Accept'] = 'application/json'
135
158
  req.headers['authorization'] = "Bearer #{token}"
159
+ if use_profile
160
+ add_spp_header(req)
161
+ end
136
162
  yield req if block_given?
137
163
  end
138
164
  log "Calling (DELETE) #{response.env.url.to_s} - took #{((Time.now - t0) * 1000).to_i} ms"
@@ -147,7 +173,7 @@ module PowerBI
147
173
  end
148
174
  end
149
175
 
150
- def post_file(url, file, params = {})
176
+ def post_file(url, file, params = {}, use_profile: true)
151
177
  t0 = Time.now
152
178
  conn = Faraday.new do |f|
153
179
  f.request :multipart
@@ -158,6 +184,9 @@ module PowerBI
158
184
  req.headers['Accept'] = 'application/json'
159
185
  req.headers['Content-Type'] = 'multipart/form-data'
160
186
  req.headers['authorization'] = "Bearer #{token}"
187
+ if use_profile
188
+ add_spp_header(req)
189
+ end
161
190
  req.body = {value: Faraday::UploadIO.new(file, 'application/octet-stream')}
162
191
  req.options.timeout = 120 # default is 60 seconds Net::ReadTimeout
163
192
  end
@@ -170,6 +199,12 @@ module PowerBI
170
199
 
171
200
  private
172
201
 
202
+ def add_spp_header(req)
203
+ if @profile_id
204
+ req.headers['X-PowerBI-Profile-Id'] = @profile_id
205
+ end
206
+ end
207
+
173
208
  def token
174
209
  @token_generator.call
175
210
  end
data/lib/power-bi.rb CHANGED
@@ -22,6 +22,8 @@ require_relative "power-bi/parameter"
22
22
  require_relative "power-bi/refresh"
23
23
  require_relative "power-bi/gateway"
24
24
  require_relative "power-bi/gateway_datasource"
25
+ require_relative "power-bi/gateway_datasource_user"
25
26
  require_relative "power-bi/page"
26
27
  require_relative "power-bi/user"
27
- require_relative "power-bi/capacity"
28
+ require_relative "power-bi/capacity"
29
+ require_relative "power-bi/profile"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: power-bi
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lode Cools
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-04 00:00:00.000000000 Z
11
+ date: 2024-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -95,9 +95,11 @@ files:
95
95
  - lib/power-bi/datasource.rb
96
96
  - lib/power-bi/gateway.rb
97
97
  - lib/power-bi/gateway_datasource.rb
98
+ - lib/power-bi/gateway_datasource_user.rb
98
99
  - lib/power-bi/object.rb
99
100
  - lib/power-bi/page.rb
100
101
  - lib/power-bi/parameter.rb
102
+ - lib/power-bi/profile.rb
101
103
  - lib/power-bi/refresh.rb
102
104
  - lib/power-bi/report.rb
103
105
  - lib/power-bi/tenant.rb