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

Add support for fine-grained access control #514

Merged
merged 1 commit into from
Dec 18, 2024
Merged
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
32 changes: 18 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,21 @@ $aws secretsmanager put-secret-value \

### Executing Optional Tasks
#### Construct Props
| Name | Type | Description |
|-----------------------------------------------------------|:---------|:-----------------------------------------------------------------------------------------|
| [useSsl](#ssl-configuration) <required> | boolean | Should the Jenkins use https |
| [restrictServerAccessTo](#restricting-server-access) <required> | Ipeer | Restrict jenkins server access |
| [authType](#setup-authentication-using-openid-connect-oidc-or-github-authentication) | string | Authentication type for Jenkins login. Acceptable values: github, oidc, default |
| [ignoreResourcesFailures](#ignore-resources-failure) | boolean | Additional verification during deployment and resource startup |
| [adminUsers](#setup-authentication-using-openid-connect-oidc-or-github-authentication) | string[] | List of users with admin access during initial deployment |
| [additionalCommands](#runnning-additional-commands) | string | Additional logic that needs to be run on Master Node. The value has to be path to a file |
| [dataRetention](#data-retention) | boolean | Do you want to retain jenkins jobs and build history |
| [agentAssumeRole](#assume-role) | string | IAM role ARN to be assumed by jenkins agent nodes |
| [envVarsFilePath](#add-environment-variables) | string | Path to file containing env variables in the form of key value pairs |
| [macAgent](#mac-agents) | boolean | Add mac agents to jenkins |
| [useProdAgents](#use-production-agents) | boolean | Should jenkins server use production agents |
| [enableViews](#enable-views) | boolean | Adds Build, Test, Release and Misc views to Jenkins Dashboard . Defaults to false |
| Name | Type | Description |
|----------------------------------------------------------------------------------------|:---------|:---------------------------------------------------------------------------------------------------------------------------|
| [useSsl](#ssl-configuration) <required> | boolean | Should the Jenkins use https |
| [restrictServerAccessTo](#restricting-server-access) <required> | Ipeer | Restrict jenkins server access |
| [authType](#setup-authentication-using-openid-connect-oidc-or-github-authentication) | string | Authentication type for Jenkins login. Acceptable values: github, oidc, default |
| [ignoreResourcesFailures](#ignore-resources-failure) | boolean | Additional verification during deployment and resource startup |
| [adminUsers](#setup-authentication-using-openid-connect-oidc-or-github-authentication) | string[] | List of users with admin access during initial deployment |
| [additionalCommands](#runnning-additional-commands) | string | Additional logic that needs to be run on Master Node. The value has to be path to a file |
| [dataRetention](#data-retention) | boolean | Do you want to retain jenkins jobs and build history |
| [agentAssumeRole](#assume-role) | string | IAM role ARN to be assumed by jenkins agent nodes |
| [envVarsFilePath](#add-environment-variables) | string | Path to file containing env variables in the form of key value pairs |
| [macAgent](#mac-agents) | boolean | Add mac agents to jenkins |
| [useProdAgents](#use-production-agents) | boolean | Should jenkins server use production agents |
| [enableViews](#enable-views) | boolean | Adds Build, Test, Release and Misc views to Jenkins Dashboard . Defaults to false |
| [FineGrainedAccessSpecs](#fine-grained-access) | FineGrainedAccessSpecs | Add specifications for fine grained access contol. See [FineGrainedAccessSpecs](lib/compute/auth-config.ts) for more details |
#### SSL Configuration
1. Locate the secret manager arns in the ci-config-stack outputs
1. Update the secret value ([see docs](https://docs.aws.amazon.com/cli/latest/reference/secretsmanager/put-secret-value.html)) for the `certContentsSecret` with the certificate contents
Expand Down Expand Up @@ -264,6 +265,9 @@ Views on Jenkins dashboard allows us to classify jobs into different sections. B

The `All` is the default view and contains all the jobs.

#### Fine Grained Access
Apart from global admin and read-only access, users can be given fine-grained access for specific workflows/folder. The access is set up using role based strategy plugin. The construct props should be of type [FineGrainedAccessSpecs](./lib/compute/auth-config.ts). Check the details for specifying [patterns](https://plugins.jenkins.io/role-strategy/#plugin-content-configuring-roles). Currently, this code base only adds `builder-template` that allows to build jobs. For adding more templates, please contribute.

### Troubleshooting
#### Main Node
Useful links
Expand Down
4 changes: 4 additions & 0 deletions lib/ci-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { JenkinsMonitoring } from './monitoring/ci-alarms';
import { JenkinsExternalLoadBalancer } from './network/ci-external-load-balancer';
import { JenkinsSecurityGroups } from './security/ci-security-groups';
import { JenkinsWAF } from './security/waf';
import { FineGrainedAccessSpecs } from './compute/auth-config';

export interface CIStackProps extends StackProps {
/** Should the Jenkins use https */
Expand All @@ -52,6 +53,8 @@ export interface CIStackProps extends StackProps {
readonly enableViews?: boolean;
/** Use Production Agents */
readonly useProdAgents?: boolean;
/** Fine grain access control specifications */
readonly fineGrainedAccessSpecs?: FineGrainedAccessSpecs[];
}

function getServerAccess(serverAccessType: string, restrictServerAccessTo: string): IPeer {
Expand Down Expand Up @@ -190,6 +193,7 @@ export class CIStack extends Stack {
authCredsSecretsArn: importedAuthConfigValuesSecretBucketValue.toString(),
useSsl,
authType,
fineGrainedAccessSpecs: props?.fineGrainedAccessSpecs,
failOnCloudInitError: props?.ignoreResourcesFailures,
adminUsers: props?.adminUsers,
agentNodeSecurityGroup: this.securityGroups.agentNodeSG.securityGroupId,
Expand Down
36 changes: 32 additions & 4 deletions lib/compute/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
* compatible open source license.
*/

export interface FineGrainedAccessSpecs {
users: string[],
roleName: string,
pattern: string,
templateName: string
}

export class AuthConfig {
private static readonly adminRolePermissions: string[] = [
'Overall/Administer',
Expand Down Expand Up @@ -50,10 +57,21 @@ export class AuthConfig {
'View/Read',
];

public static addOidcConfigToJenkinsYaml(yamlObject: any, authType: string, admins?: string[]): any {
private static readonly builderTemplatePermissions: string[] = [
'Job/Build',
'Job/Cancel',
'Job/Discover',
'Job/Read',
'Lockable Resources/View',
'Run/Replay',
'Metrics/View',
'View/Read',
];

public static addOidcConfigToJenkinsYaml(yamlObject: any, authType: string, admins?: string[], fineGrainedAccessItems?: FineGrainedAccessSpecs[]): any {
const jenkinsYaml: any = yamlObject;
let adminUsers: string[] = ['admin'];
const readOnlyUsers: string[] = ['anonymous'];
const readOnlyUsers: string[] = ['anonymous', 'authenticated'];

if (admins) {
adminUsers = adminUsers.concat(admins);
Expand Down Expand Up @@ -89,6 +107,10 @@ export class AuthConfig {

const rolesAndPermissions: { [x: string]: any; } = {
roleBased: {
permissionTemplates: [{
name: 'builder-template',
permissions: AuthConfig.builderTemplatePermissions,
}],
roles: {
global: [{
entries: adminUsers.map((user) => ({ user })),
Expand All @@ -103,14 +125,20 @@ export class AuthConfig {
pattern: '.*',
permissions: AuthConfig.readOnlyRolePermissions,
},

],
},
},
};

jenkinsYaml.jenkins.authorizationStrategy = rolesAndPermissions;

if (typeof fineGrainedAccessItems !== 'undefined') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does !== undefined better than === true or similar?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both have different purpose. One checks if value is undefined and other one checks if value is truly assigned to the variable.

jenkinsYaml.jenkins.authorizationStrategy.roleBased.roles.items = fineGrainedAccessItems.map((item) => ({
entries: item.users.map((user) => ({ user })),
name: item.roleName,
pattern: item.pattern,
templateName: item.templateName,
}));
}
if (authType === 'github') {
jenkinsYaml.jenkins.securityRealm = githubAuthConfig;
} else {
Expand Down
6 changes: 4 additions & 2 deletions lib/compute/jenkins-main-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { join } from 'path';
import { CloudwatchAgent } from '../constructs/cloudwatch-agent';
import { AgentNodeConfig, AgentNodeNetworkProps, AgentNodeProps } from './agent-node-config';
import { EnvConfig } from './env-config';
import { AuthConfig } from './auth-config';
import { AuthConfig, FineGrainedAccessSpecs } from './auth-config';
import { ViewsConfig } from './views';

interface HttpConfigProps {
Expand All @@ -51,6 +51,7 @@ interface LoginAuthProps {
readonly authCredsSecretsArn: string;
readonly authType: string;
readonly adminUsers?: string[];
readonly fineGrainedAccessSpecs?: FineGrainedAccessSpecs[];
}

interface DataRetentionProps {
Expand Down Expand Up @@ -452,7 +453,8 @@ export class JenkinsMainNode {
agentNodeObject: AgentNodeConfig, props: AgentNodeNetworkProps, agentNode: AgentNodeProps[], macAgent: string): string {
let updatedConfig = agentNodeObject.addAgentConfigToJenkinsYaml(stack, agentNode, props, macAgent);
if (loginAuthProps.authType !== 'default') {
updatedConfig = AuthConfig.addOidcConfigToJenkinsYaml(updatedConfig, loginAuthProps.authType, loginAuthProps.adminUsers);
updatedConfig = AuthConfig.addOidcConfigToJenkinsYaml(updatedConfig, loginAuthProps.authType,
loginAuthProps.adminUsers, loginAuthProps.fineGrainedAccessSpecs);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will admins have all the permissions defined in above?
And what about read only users?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Admin users will have admin permissions. Same applies for readonly users. The roles are divided into 2 types. global and items at jenkins level. Admin and readonly falls under global and fineGrainedAccess falls under items category.

}
if (jenkinsMainNodeProps.envVarsFilePath !== '' && jenkinsMainNodeProps.envVarsFilePath != null) {
updatedConfig = EnvConfig.addEnvConfigToJenkinsYaml(updatedConfig, jenkinsMainNodeProps.envVarsFilePath);
Expand Down
24 changes: 12 additions & 12 deletions test/ci-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { CIStack } from '../lib/ci-stack';
test('CI Stack Basic Resources', () => {
const app = new App({
context: {
useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32', additionalCommands: './test/data/hello-world.py',
useSsl: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32', additionalCommands: './test/data/hello-world.py',
},
});

Expand Down Expand Up @@ -45,7 +45,7 @@ test('CI Stack Basic Resources', () => {
test('External security group is open', () => {
const app = new App({
context: {
useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: 'all',
useSsl: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: 'all',
},
});

Expand Down Expand Up @@ -89,7 +89,7 @@ test('External security group is open', () => {
test('External security group is restricted', () => {
const app = new App({
context: {
useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '10.0.0.0/24',
useSsl: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '10.0.0.0/24',
},
});

Expand Down Expand Up @@ -135,7 +135,7 @@ test('External security group is restricted', () => {
test('MainNode', () => {
const app = new App({
context: {
useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32',
useSsl: 'true', authType: 'oidc', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32',
},
});

Expand Down Expand Up @@ -167,7 +167,7 @@ test('MainNode', () => {
test('LoadBalancer', () => {
const app = new App({
context: {
useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: 'all',
useSsl: 'true', authType: 'oidc', serverAccessType: 'ipv4', restrictServerAccessTo: 'all',
},
});

Expand All @@ -192,7 +192,7 @@ test('LoadBalancer', () => {
test('CloudwatchCpuAlarm', () => {
const app = new App({
context: {
useSsl: 'false', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32',
useSsl: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32',
},
});

Expand All @@ -211,7 +211,7 @@ test('CloudwatchCpuAlarm', () => {
test('CloudwatchMemoryAlarm', () => {
const app = new App({
context: {
useSsl: 'false', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32',
useSsl: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32',
},
});

Expand All @@ -230,7 +230,7 @@ test('CloudwatchMemoryAlarm', () => {
test('LoadBalancer Access Logging', () => {
const app = new App({
context: {
useSsl: 'false', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32',
useSsl: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32',
},
});

Expand Down Expand Up @@ -367,7 +367,7 @@ test('LoadBalancer Access Logging', () => {
test('WAF rules', () => {
const app = new App({
context: {
useSsl: 'false', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '0.0.0.0/0',
useSsl: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '0.0.0.0/0',
},
});

Expand Down Expand Up @@ -452,7 +452,7 @@ test('WAF rules', () => {
test('Test WAF association with ALB', () => {
const app = new App({
context: {
useSsl: 'false', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '0.0.0.0/0',
useSsl: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '0.0.0.0/0',
},
});

Expand All @@ -478,7 +478,7 @@ test('Test WAF association with ALB', () => {
test('Test configElement jenkins content to use X-Forwarded-For header on port 443', () => {
const app = new App({
context: {
useSsl: 'true', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '0.0.0.0/0',
useSsl: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '0.0.0.0/0',
},
});

Expand Down Expand Up @@ -512,7 +512,7 @@ test('Test configElement jenkins content to use X-Forwarded-For header on port 4
test('Test configElement jenkins content to use X-Forwarded-For header on port 80', () => {
const app = new App({
context: {
useSsl: 'false', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '0.0.0.0/0',
useSsl: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '0.0.0.0/0',
},
});

Expand Down
Loading
Loading