Skip to content
Closed
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
34 changes: 34 additions & 0 deletions crates/openshell-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@ pub struct Config {
/// Gateway user authentication behavior.
pub auth: GatewayAuthConfig,

/// Multi-tenant resource ownership enforcement.
pub ownership: OwnershipConfigCore,

/// mTLS user authentication configuration. When enabled, a verified TLS
/// client certificate can authenticate CLI/SDK callers as a
/// `Principal::User`. This is for local single-user gateways only;
Expand Down Expand Up @@ -490,6 +493,13 @@ pub struct OidcConfig {
/// Keycloak: `scope` (space-delimited string). Okta: `scp` (JSON array).
#[serde(default)]
pub scopes_claim: String,

/// Dot-separated path to the groups array in the JWT claims.
/// Used for multi-tenant deployments where group membership maps to
/// organizational tenants.
/// Defaults to `groups`.
#[serde(default = "default_groups_claim")]
pub groups_claim: String,
}

/// mTLS user authentication for local, single-user gateways.
Expand All @@ -515,6 +525,25 @@ pub struct GatewayAuthConfig {
pub allow_unauthenticated_users: bool,
}

/// Multi-tenant resource ownership configuration.
///
/// When enabled, the gateway stamps every created sandbox and provider with
/// the caller's identity subject (`openshell.ai/owner`) and an optional tenant
/// identifier (`openshell.ai/tenant`). Subsequent get/list/delete operations
/// enforce that only the resource owner (or an admin) can access the resource.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OwnershipConfigCore {
/// Enable per-resource ownership enforcement.
#[serde(default)]
pub enabled: bool,

/// Optional tenant identifier for gateway-per-tenant deployments.
/// When set, every created resource is labeled with this value.
#[serde(default)]
pub tenant_id: Option<String>,
}

const fn default_jwks_ttl_secs() -> u64 {
3600
}
Expand Down Expand Up @@ -564,6 +593,10 @@ fn default_user_role() -> String {
"openshell-user".to_string()
}

fn default_groups_claim() -> String {
"groups".to_string()
}

impl Config {
/// Create a new config with optional TLS.
pub fn new(tls: Option<TlsConfig>) -> Self {
Expand All @@ -575,6 +608,7 @@ impl Config {
tls,
oidc: None,
auth: GatewayAuthConfig::default(),
ownership: OwnershipConfigCore::default(),
mtls_auth: MtlsAuthConfig::default(),
gateway_jwt: None,
database_url: String::new(),
Expand Down
8 changes: 8 additions & 0 deletions crates/openshell-core/src/driver_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ pub const LABEL_SANDBOX_NAME: &str = "openshell.ai/sandbox-name";
/// Container/pod label carrying the sandbox namespace.
pub const LABEL_SANDBOX_NAMESPACE: &str = "openshell.ai/sandbox-namespace";

/// Container/pod label carrying the identity subject of the user who created
/// the sandbox. Used for multi-tenant ownership enforcement.
pub const LABEL_OWNER: &str = "openshell.ai/owner";

/// Container/pod label carrying the organizational tenant identifier.
/// Set from the gateway's configured `tenant_id` in multi-tenant deployments.
pub const LABEL_TENANT: &str = "openshell.ai/tenant";

// ---------------------------------------------------------------------------

/// Path to the sandbox supervisor binary inside the container image.
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub mod time;

pub use config::{
ComputeDriverKind, Config, GatewayAuthConfig, GatewayJwtConfig, MtlsAuthConfig, OidcConfig,
TlsConfig,
OwnershipConfigCore, TlsConfig,
};
pub use error::{ComputeDriverError, Error, Result};
pub use metadata::{GetResourceVersion, ObjectId, ObjectLabels, ObjectName, SetResourceVersion};
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-server/src/auth/authenticator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ mod tests {
display_name: None,
roles: vec![],
scopes: vec![],
groups: vec![],
provider: IdentityProvider::Oidc,
},
})
Expand Down
2 changes: 2 additions & 0 deletions crates/openshell-server/src/auth/authz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ mod tests {
display_name: None,
roles: roles.iter().map(|r| (*r).to_string()).collect(),
scopes: vec![],
groups: vec![],
provider: IdentityProvider::Oidc,
}
}
Expand All @@ -164,6 +165,7 @@ mod tests {
display_name: None,
roles: roles.iter().map(|r| (*r).to_string()).collect(),
scopes: scopes.iter().map(|s| (*s).to_string()).collect(),
groups: vec![],
provider: IdentityProvider::Oidc,
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-server/src/auth/guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ mod tests {
display_name: None,
roles: vec![],
scopes: vec![],
groups: vec![],
provider: IdentityProvider::Oidc,
},
})
Expand Down
5 changes: 5 additions & 0 deletions crates/openshell-server/src/auth/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ pub struct Identity {
/// `OAuth2` scopes granted to this identity. Empty when scope enforcement is disabled.
pub scopes: Vec<String>,

/// Group memberships extracted from the JWT claims.
/// Used for multi-tenant deployments where group membership maps to
/// organizational tenants.
pub groups: Vec<String>,

/// Which authentication provider produced this identity.
pub provider: IdentityProvider,
}
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-server/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod identity;
pub mod k8s_sa;
pub mod method_authz;
pub mod oidc;
pub mod ownership;
pub mod principal;
pub mod sandbox_jwt;
pub mod sandbox_methods;
Expand Down
29 changes: 29 additions & 0 deletions crates/openshell-server/src/auth/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ pub struct OidcClaims {
/// Roles extracted from the configurable claim path.
#[serde(skip)]
pub roles: Vec<String>,
/// Groups extracted from the configurable claim path.
#[serde(skip)]
pub groups: Vec<String>,
/// Raw claims for flexible role extraction.
#[serde(flatten)]
extra: serde_json::Value,
Expand Down Expand Up @@ -162,6 +165,30 @@ impl OidcClaims {
.filter(|s| !STANDARD_OIDC_SCOPES.contains(&s.as_str()))
.collect()
}

/// Extract groups from the JWT claims using a dot-separated path.
///
/// Follows the same pattern as [`extract_roles`] — traverses a
/// dot-delimited claim path and collects the resulting JSON array
/// into `self.groups`.
fn extract_groups(&mut self, groups_claim: &str) {
if groups_claim.is_empty() {
return;
}
let mut value = &self.extra;
for segment in groups_claim.split('.') {
match value.get(segment) {
Some(v) => value = v,
None => return,
}
}
if let Some(arr) = value.as_array() {
self.groups = arr
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
}
}
}

impl JwksCache {
Expand Down Expand Up @@ -332,6 +359,7 @@ impl JwksCache {

let mut claims = token_data.claims;
claims.extract_roles(&self.config.roles_claim);
claims.extract_groups(&self.config.groups_claim);

let scopes = if self.config.scopes_claim.is_empty() {
vec![]
Expand All @@ -344,6 +372,7 @@ impl JwksCache {
display_name: claims.preferred_username,
roles: claims.roles,
scopes,
groups: claims.groups,
provider: IdentityProvider::Oidc,
})
}
Expand Down
Loading
Loading