1
- use anyhow:: { Context , Result } ;
2
- use azure_pim_cli:: {
3
- az_cli:: get_token,
4
- elevate:: { elevate_role, ElevateConfig } ,
5
- roles:: list,
6
- } ;
1
+ use anyhow:: { bail, Context , Result } ;
2
+ use azure_pim_cli:: { activate:: activate_role, az_cli:: get_token, roles:: list_roles} ;
7
3
use clap:: { Command , CommandFactory , Parser , Subcommand } ;
8
- use std:: { cmp:: min, io:: stdout} ;
4
+ use serde:: Deserialize ;
5
+ use std:: {
6
+ cmp:: min, collections:: BTreeSet , error:: Error , fs:: File , io:: stdout, path:: PathBuf ,
7
+ str:: FromStr ,
8
+ } ;
9
+ use tracing:: { error, info} ;
9
10
10
11
#[ derive( Parser ) ]
11
12
#[ command(
@@ -25,14 +26,76 @@ enum SubCommand {
25
26
/// List eligible assignments
26
27
List ,
27
28
28
- /// Elevate to a specific role
29
- Elevate ( ElevateConfig ) ,
29
+ /// Activate a specific role
30
+ Activate {
31
+ /// Name of the role to elevate
32
+ role : String ,
33
+ /// Scope to elevate
34
+ scope : String ,
35
+ /// Justification for the request
36
+ justification : String ,
37
+ /// Duration in minutes
38
+ #[ clap( long, default_value_t = 480 ) ]
39
+ duration : u32 ,
40
+ } ,
41
+
42
+ /// Activate a set of roles
43
+ ///
44
+ /// This command can be used to activate multiple roles at once. It can be
45
+ /// used with a config file or by specifying roles on the command line.
46
+ ActivateSet {
47
+ /// Justification for the request
48
+ justification : String ,
49
+ #[ clap( long, default_value_t = 480 ) ]
50
+ /// Duration in minutes
51
+ duration : u32 ,
52
+ #[ clap( long) ]
53
+ /// Path to a JSON config file containing a set of roles to elevate
54
+ ///
55
+ /// Example config file:
56
+ /// `
57
+ /// [
58
+ /// {
59
+ /// "scope": "/subscriptions/00000000-0000-0000-0000-000000000000",
60
+ /// "role": "Owner"
61
+ /// },
62
+ /// {
63
+ /// "scope": "/subscriptions/00000000-0000-0000-0000-000000000001",
64
+ /// "role": "Owner"
65
+ /// }
66
+ /// ]
67
+ /// `
68
+ config : Option < PathBuf > ,
69
+ #[ clap( long, conflicts_with = "config" , value_name = "SCOPE=NAME" , value_parser = parse_key_val:: <String , String >, action = clap:: ArgAction :: Append ) ]
70
+ /// Specify a role to elevate
71
+ ///
72
+ /// Specify multiple times to include multiple key/value pairs
73
+ role : Option < Vec < ( String , String ) > > ,
74
+ } ,
30
75
31
76
#[ command( hide = true ) ]
32
77
Readme ,
33
78
}
34
79
35
- fn build_readme ( cmd : & mut Command , mut names : Vec < String > ) -> String {
80
+ /// Parse a single key-value pair of `X=Y` into a typed tuple of `(X, Y)`.
81
+ ///
82
+ /// # Errors
83
+ /// Returns an `Err` if any of the keys or values cannot be parsed or if no `=` is found.
84
+ pub fn parse_key_val < T , U > ( s : & str ) -> Result < ( T , U ) , Box < dyn Error + Send + Sync + ' static > >
85
+ where
86
+ T : FromStr ,
87
+ T :: Err : Error + Send + Sync + ' static ,
88
+ U : FromStr ,
89
+ U :: Err : Error + Send + Sync + ' static ,
90
+ {
91
+ if let Some ( ( key, value) ) = s. split_once ( '=' ) {
92
+ Ok ( ( key. parse ( ) ?, value. parse ( ) ?) )
93
+ } else {
94
+ Err ( format ! ( "invalid KEY=value: no `=` found in `{s}`" ) . into ( ) )
95
+ }
96
+ }
97
+
98
+ fn build_readme_entry ( cmd : & mut Command , mut names : Vec < String > ) -> String {
36
99
let mut readme = String :: new ( ) ;
37
100
let base_name = cmd. get_name ( ) . to_owned ( ) ;
38
101
@@ -60,11 +123,37 @@ fn build_readme(cmd: &mut Command, mut names: Vec<String>) -> String {
60
123
if cmd. get_name ( ) == "readme" {
61
124
continue ;
62
125
}
63
- readme. push_str ( & build_readme ( cmd, names. clone ( ) ) ) ;
126
+ readme. push_str ( & build_readme_entry ( cmd, names. clone ( ) ) ) ;
64
127
}
65
128
readme
66
129
}
67
130
131
+ fn build_readme ( ) {
132
+ let mut cmd = Cmd :: command ( ) ;
133
+ let readme = build_readme_entry ( & mut cmd, Vec :: new ( ) )
134
+ . replace ( "azure-pim-cli" , "az-pim" )
135
+ . replacen (
136
+ "# az-pim" ,
137
+ & format ! ( "# Azure PIM CLI\n \n {}" , env!( "CARGO_PKG_DESCRIPTION" ) ) ,
138
+ 1 ,
139
+ )
140
+ . lines ( )
141
+ . map ( str:: trim_end)
142
+ . collect :: < Vec < _ > > ( )
143
+ . join ( "\n " )
144
+ . replace ( "\n \n \n " , "\n " ) ;
145
+ print ! ( "{readme}" ) ;
146
+ }
147
+
148
+ #[ derive( Deserialize ) ]
149
+ struct Role {
150
+ scope : String ,
151
+ role : String ,
152
+ }
153
+
154
+ #[ derive( Deserialize ) ]
155
+ struct Roles ( Vec < Role > ) ;
156
+
68
157
fn main ( ) -> Result < ( ) > {
69
158
tracing_subscriber:: fmt ( )
70
159
. with_env_filter (
@@ -80,31 +169,93 @@ fn main() -> Result<()> {
80
169
match args. commands {
81
170
SubCommand :: List => {
82
171
let token = get_token ( ) . context ( "unable to obtain access token" ) ?;
83
- let roles = list ( & token) . context ( "unable to list available roles in PIM" ) ?;
172
+ let roles = list_roles ( & token) . context ( "unable to list available roles in PIM" ) ?;
84
173
serde_json:: to_writer_pretty ( stdout ( ) , & roles) ?;
85
174
Ok ( ( ) )
86
175
}
87
- SubCommand :: Elevate ( config) => {
176
+ SubCommand :: Activate {
177
+ role,
178
+ scope,
179
+ justification,
180
+ duration,
181
+ } => {
88
182
let token = get_token ( ) . context ( "unable to obtain access token" ) ?;
89
- let roles = list ( & token) . context ( "unable to list available roles in PIM" ) ?;
90
- elevate_role ( & token, & config, & roles) . context ( "unable to elevate to specified role" ) ?;
183
+ let roles = list_roles ( & token) . context ( "unable to list available roles in PIM" ) ?;
184
+ let entry = roles
185
+ . iter ( )
186
+ . find ( |v| v. role == role && v. scope == scope)
187
+ . context ( "role not found" ) ?;
188
+
189
+ info ! ( "activating {role:?} in {}" , entry. scope_name) ;
190
+
191
+ activate_role (
192
+ & token,
193
+ & entry. scope ,
194
+ & entry. role_definition_id ,
195
+ & justification,
196
+ duration,
197
+ )
198
+ . context ( "unable to elevate to specified role" ) ?;
199
+ Ok ( ( ) )
200
+ }
201
+ SubCommand :: ActivateSet {
202
+ config,
203
+ role,
204
+ justification,
205
+ duration,
206
+ } => {
207
+ let mut desired_roles = role
208
+ . unwrap_or_default ( )
209
+ . into_iter ( )
210
+ . collect :: < BTreeSet < _ > > ( ) ;
211
+
212
+ if let Some ( path) = config {
213
+ let handle = File :: open ( path) . context ( "unable to open activate-set config file" ) ?;
214
+ let Roles ( roles) =
215
+ serde_json:: from_reader ( handle) . context ( "unable to parse config file" ) ?;
216
+ for entry in roles {
217
+ desired_roles. insert ( ( entry. scope , entry. role ) ) ;
218
+ }
219
+ }
220
+
221
+ if desired_roles. is_empty ( ) {
222
+ bail ! ( "no roles specified" ) ;
223
+ }
224
+
225
+ let token = get_token ( ) . context ( "unable to obtain access token" ) ?;
226
+ let available = list_roles ( & token) . context ( "unable to list available roles in PIM" ) ?;
227
+
228
+ let mut to_add = BTreeSet :: new ( ) ;
229
+ for ( scope, role) in & desired_roles {
230
+ let entry = & available
231
+ . iter ( )
232
+ . find ( |v| & v. role == role && & v. scope == scope)
233
+ . with_context ( || format ! ( "role not found. role:{role} scope:{scope}" ) ) ?;
234
+
235
+ to_add. insert ( ( scope, role, & entry. role_definition_id , & entry. scope_name ) ) ;
236
+ }
237
+
238
+ let mut success = true ;
239
+ for ( scope, role, role_definition_id, scope_name) in to_add {
240
+ info ! ( "activating {role:?} in {scope_name}" ) ;
241
+ if let Err ( error) =
242
+ activate_role ( & token, scope, role_definition_id, & justification, duration)
243
+ {
244
+ error ! (
245
+ "scope: {scope} role_definition_id: {role_definition_id} error: {error:?}"
246
+ ) ;
247
+ success = false ;
248
+ }
249
+ }
250
+
251
+ if !success {
252
+ bail ! ( "unable to elevate to all roles" ) ;
253
+ }
254
+
91
255
Ok ( ( ) )
92
256
}
93
257
SubCommand :: Readme => {
94
- let mut cmd = Cmd :: command ( ) ;
95
- let readme = build_readme ( & mut cmd, Vec :: new ( ) )
96
- . replace ( "azure-pim-cli" , "az-pim" )
97
- . replacen (
98
- "# az-pim" ,
99
- & format ! ( "# Azure PIM CLI\n \n {}" , env!( "CARGO_PKG_DESCRIPTION" ) ) ,
100
- 1 ,
101
- )
102
- . lines ( )
103
- . map ( str:: trim_end)
104
- . collect :: < Vec < _ > > ( )
105
- . join ( "\n " )
106
- . replace ( "\n \n \n " , "\n " ) ;
107
- print ! ( "{readme}" ) ;
258
+ build_readme ( ) ;
108
259
Ok ( ( ) )
109
260
}
110
261
}
0 commit comments