Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable metadata in config to declare if admin is required or not #351

Merged
merged 5 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ if (!$SkipBuild) {
# projects are in dependency order
$projects = @(
"tree-sitter-dscexpression",
"security_context_lib",
"dsc_lib",
"file_lib",
"dsc",
Expand Down
3 changes: 3 additions & 0 deletions dsc/examples/groups.dsc.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Example for grouping and groups in groups
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json
metadata:
Microsoft.DSC:
requiredSecurityContext: Current # this is the default and just used as an example indicating this config works for admins and non-admins
resources:
- name: Last Group
type: Microsoft.DSC/Group
Expand Down
11 changes: 11 additions & 0 deletions dsc/examples/require_admin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# example showing use of specific metadata to indicate this config requires admin to run
# note that the resource doesn't require admin, but this will fail to even try to run the
# config if the user is not root or elevated as administrator
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json
metadata:
Microsoft.DSC:
requiredSecurityContext: Elevated
resources:
- name: os
type: Microsoft/OSInfo
properties: {}
10 changes: 10 additions & 0 deletions dsc/examples/require_nonadmin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# example showing use of specific metadata to indicate this config requires admin to run
# this will fail to even try to run the config if the user is root or elevated as administrator
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json
metadata:
Microsoft.DSC:
requiredSecurityContext: Restricted
resources:
- name: os
type: Microsoft/OSInfo
properties: {}
36 changes: 36 additions & 0 deletions dsc/tests/dsc_securitycontext.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'Tests for configuration security context metadata' {
BeforeAll {
$isAdmin = if ($IsWindows) {
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
[System.Security.Principal.WindowsPrincipal]::new($identity).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}
else {
[System.Environment]::UserName -eq 'root'
}
}

It 'Require admin' {
$out = dsc config get -p $PSScriptRoot/../examples/require_admin.yaml
if ($isAdmin) {
$LASTEXITCODE | Should -Be 0
$out | Should -Not -BeNullOrEmpty
}
else {
$LASTEXITCODE | Should -Be 2
}
}

It 'Require non-admin' {
$out = dsc config get -p $PSScriptRoot/../examples/require_nonadmin.yaml
if ($isAdmin) {
$LASTEXITCODE | Should -Be 2
}
else {
$LASTEXITCODE | Should -Be 0
$out | Should -Not -BeNullOrEmpty
}
}
}
1 change: 1 addition & 0 deletions dsc_lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ serde_json = { version = "1.0", features = ["preserve_order"] }
serde_yaml = { version = "0.9.3" }
thiserror = "1.0"
chrono = "0.4.26"
security_context_lib = { path = "../security_context_lib" }
tracing = "0.1.37"
tree-sitter = "0.20"
tree-sitter-dscexpression = { path = "../tree-sitter-dscexpression" }
Expand Down
29 changes: 28 additions & 1 deletion dsc_lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::collections::HashMap;

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub enum ContextKind {
Configuration,
Resource,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub enum SecurityContextKind {
Current,
Elevated,
Restricted,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct MicrosoftDscMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<ContextKind>,
#[serde(rename = "requiredSecurityContext", skip_serializing_if = "Option::is_none")]
pub required_security_context: Option<SecurityContextKind>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct Metadata {
#[serde(rename = "Microsoft.DSC", skip_serializing_if = "Option::is_none")]
pub microsoft: Option<MicrosoftDscMetadata>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Configuration {
Expand All @@ -18,7 +45,7 @@ pub struct Configuration {
pub variables: Option<HashMap<String, Value>>,
pub resources: Vec<Resource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, Value>>,
pub metadata: Option<Metadata>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
Expand Down
34 changes: 33 additions & 1 deletion dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ use crate::DscResource;
use crate::discovery::Discovery;
use crate::parser::Statement;
use self::context::Context;
use self::config_doc::{Configuration, DataType};
use self::config_doc::{Configuration, DataType, SecurityContextKind};
use self::depends_on::get_resource_invocation_order;
use self::config_result::{ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult, ConfigurationExportResult};
use self::contraints::{check_length, check_number_limits, check_allowed_values};
use indicatif::{ProgressBar, ProgressStyle};
use security_context_lib::{SecurityContext, get_security_context};
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::time::Duration;
Expand Down Expand Up @@ -161,6 +162,36 @@ fn add_metadata(kind: &Kind, mut properties: Option<Map<String, Value>> ) -> Res
Ok(serde_json::to_string(&properties)?)
}

fn check_security_context(config: &Configuration) -> Result<(), DscError> {
tgauth marked this conversation as resolved.
Show resolved Hide resolved
if config.metadata.is_none() {
return Ok(());
}

if let Some(metadata) = &config.metadata {
if let Some(microsoft_dsc) = &metadata.microsoft {
if let Some(required_security_context) = &microsoft_dsc.required_security_context {
match required_security_context {
SecurityContextKind::Current => {
// no check needed
},
SecurityContextKind::Elevated => {
if get_security_context() != SecurityContext::Admin {
return Err(DscError::SecurityContext("Elevated security context required".to_string()));
}
},
SecurityContextKind::Restricted => {
if get_security_context() != SecurityContext::User {
return Err(DscError::SecurityContext("Restricted security context required".to_string()));
}
},
}
}
}
}

Ok(())
}

impl Configurator {
/// Create a new `Configurator` instance.
///
Expand Down Expand Up @@ -415,6 +446,7 @@ impl Configurator {

fn validate_config(&mut self) -> Result<Configuration, DscError> {
let config: Configuration = serde_json::from_str(self.config.as_str())?;
check_security_context(&config)?;

// Perform discovery of resources used in config
let mut required_resources = config.resources.iter().map(|p| p.resource_type.to_lowercase()).collect::<Vec<String>>();
Expand Down
3 changes: 3 additions & 0 deletions dsc_lib/src/dscerror.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ pub enum DscError {
#[error("No Schema: {0}")]
SchemaNotAvailable(String),

#[error("Security context: {0}")]
SecurityContext(String),

#[error("Utf-8 conversion error: {0}")]
Utf8Conversion(#[from] Utf8Error),

Expand Down
2 changes: 1 addition & 1 deletion ntstatuserror/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub struct NtStatusError {

impl NtStatusError {
/// Create a new `NtStatusError` from an NTSTATUS error code and a message.
///
///
/// # Arguments
///
/// * `status` - The NTSTATUS error code
Expand Down
10 changes: 10 additions & 0 deletions security_context_lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "security_context_lib"
version = "0.1.0"
edition = "2021"

[target.'cfg(target_os = "windows")'.dependencies]
is_elevated = "0.1.0"

[target.'cfg(not(target_os = "windows"))'.dependencies]
nix = { version = "0.28.0", features = ["user"] }
29 changes: 29 additions & 0 deletions security_context_lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SecurityContext {
Admin,
User,
}

#[cfg(target_os = "windows")]
#[must_use]
pub fn get_security_context() -> SecurityContext {
use is_elevated::is_elevated;
if is_elevated() {
return SecurityContext::Admin;
}
SecurityContext::User
}

#[cfg(not(target_os = "windows"))]
#[must_use]
pub fn get_security_context() -> SecurityContext {
use nix::unistd::Uid;

if Uid::effective().is_root() {
return SecurityContext::Admin;
}
SecurityContext::User
}
Loading