# GitClassic editor access to CodeCommit, template v2.
#
# OPTIONAL. Creates a second IAM role, used only when someone on your team makes a
# change in GitClassic: commenting on, approving, merging, opening or closing a
# pull request; editing, adding or deleting a file (one commit at a time);
# creating a branch; or creating a repository. Viewing keeps using the read-only
# role, which this doesn't change.
#
# - Same rules as the read-only role: no access keys, your ExternalId required,
#   at most an hour per session, and every session names the GitClassic user who
#   took the action, so each change shows up in CloudTrail and in CodeCommit under
#   that person.
# - RepoPrefix limits it to repositories whose names start with that prefix
#   (including the names of repositories it creates).
# - ProtectedBranches (comma-separated, e.g. main,release): AWS refuses direct
#   commits to those branches, so changes to them go through a pull request.
#   Merging an approved pull request into them still works.
# - It can't push with git, force-push, rewrite history, delete branches or
#   repositories, rename repositories, or change approval rules, and merges still
#   have to meet your approval rules.
# - Issues and boards live in one DynamoDB table in your account, created here
#   (gitclassic-issues-<ConnectionId>, on-demand, point-in-time recovery on). The
#   role can use only that table. Deleting this stack KEEPS the table and your
#   issues; delete the table yourself if you want them gone.
# - To turn editing off, delete this stack. Viewing keeps working.
#
# Plain-English version: https://gitclassic.com/security#editor

AWSTemplateFormatVersion: '2010-09-09'
Description: >-
  GitClassic editor access (v2). An optional role that lets GitClassic work on pull
  requests, commit edits made in its editor, create branches and repositories, and keep issues and
  boards in a DynamoDB table in this account, when a user asks. Delete this stack to turn editing off
  (the issues table is kept).

Parameters:
  ConnectionId:
    Type: String
    Description: Identifies this connection in GitClassic. Filled in for you.
    AllowedPattern: '[0-9a-f]{8}'
  ExternalId:
    Type: String
    Description: GitClassic must present this value to use the role. Filled in for you.
    AllowedPattern: '[0-9a-f]{32}'
  RepoPrefix:
    Type: String
    Default: ''
    Description: Only repositories whose names start with this. Leave empty for every repository.
    AllowedPattern: '[\w.-]{0,100}'
  ProtectedBranches:
    Type: String
    Default: ''
    Description: Branches that only change through pull requests (comma-separated, e.g. main,release). Leave empty to allow direct commits everywhere.
    AllowedPattern: '(|[\w./-]+(,[\w./-]+)*)'
  CreateIssuesTable:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: "'false' only when the issues table from an earlier editor stack is still in this account (GitClassic fills this in)."

Conditions:
  HasProtectedBranches: !Not [!Equals [!Ref ProtectedBranches, '']]
  CreatesIssuesTable: !Equals [!Ref CreateIssuesTable, 'true']

Resources:
  EditorRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 'gitclassic-editor-${ConnectionId}'
      Description: Editing for GitClassic (gitclassic.com). Delete the CloudFormation stack to turn it off.
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          # GitClassic may use this role only with your ExternalId, and only when the
          # session names the GitClassic user it's for (so CloudTrail always can).
          - Sid: GitClassicWithExternalId
            Effect: Allow
            Principal:
              AWS: 'arn:aws:iam::692859917589:root'
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Ref ExternalId
              'Null':
                sts:SourceIdentity: 'false'
          # Lets that session carry the user's email as its source identity. On its
          # own it grants nothing: it only applies within an AssumeRole allowed above.
          - Sid: NameTheUser
            Effect: Allow
            Principal:
              AWS: 'arn:aws:iam::692859917589:root'
            Action: sts:SetSourceIdentity
      Policies:
        - PolicyName: gitclassic-editor
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # Comment on pull requests
              - Sid: CommentOnPullRequests
                Effect: Allow
                Action:
                  - codecommit:PostCommentForPullRequest
                  - codecommit:PostCommentReply
                  - codecommit:PostCommentForComparedCommit
                  - codecommit:UpdateComment
                  - codecommit:DeleteCommentContent
                  - codecommit:PutCommentReaction
                Resource: !Sub 'arn:aws:codecommit:*:${AWS::AccountId}:${RepoPrefix}*'
              # Approve pull requests
              - Sid: ApprovePullRequests
                Effect: Allow
                Action:
                  - codecommit:UpdatePullRequestApprovalState
                Resource: !Sub 'arn:aws:codecommit:*:${AWS::AccountId}:${RepoPrefix}*'
              # Merge pull requests
              - Sid: MergePullRequests
                Effect: Allow
                Action:
                  - codecommit:MergePullRequestByFastForward
                  - codecommit:MergePullRequestBySquash
                  - codecommit:MergePullRequestByThreeWay
                Resource: !Sub 'arn:aws:codecommit:*:${AWS::AccountId}:${RepoPrefix}*'
              # Open, close and edit pull requests
              - Sid: ManagePullRequests
                Effect: Allow
                Action:
                  - codecommit:CreatePullRequest
                  - codecommit:UpdatePullRequestStatus
                  - codecommit:UpdatePullRequestTitle
                  - codecommit:UpdatePullRequestDescription
                Resource: !Sub 'arn:aws:codecommit:*:${AWS::AccountId}:${RepoPrefix}*'
              # Commit changes made in GitClassic
              - Sid: CommitChanges
                Effect: Allow
                Action:
                  - codecommit:PutFile
                  - codecommit:CreateCommit
                  - codecommit:DeleteFile
                Resource: !Sub 'arn:aws:codecommit:*:${AWS::AccountId}:${RepoPrefix}*'
              # Create branches
              - Sid: CreateBranches
                Effect: Allow
                Action:
                  - codecommit:CreateBranch
                Resource: !Sub 'arn:aws:codecommit:*:${AWS::AccountId}:${RepoPrefix}*'
              # Create repositories
              - Sid: CreateRepositories
                Effect: Allow
                Action:
                  - codecommit:CreateRepository
                  - codecommit:UpdateRepositoryDescription
                  - codecommit:UpdateDefaultBranch
                Resource: !Sub 'arn:aws:codecommit:*:${AWS::AccountId}:${RepoPrefix}*'
              # Keep your issues and boards
              - Sid: IssuesAndBoards
                Effect: Allow
                Action:
                  - dynamodb:GetItem
                  - dynamodb:BatchGetItem
                  - dynamodb:Query
                  - dynamodb:PutItem
                  - dynamodb:UpdateItem
                  - dynamodb:DeleteItem
                  - dynamodb:DescribeTable
                Resource:
                  - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/gitclassic-issues-${ConnectionId}'
                  - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/gitclassic-issues-${ConnectionId}/index/*'
              # Write to repositories encrypted with your own KMS key
              - Sid: UseKeyViaCodeCommit
                Effect: Allow
                Action:
                  - kms:Decrypt
                  - kms:Encrypt
                  - kms:GenerateDataKey
                Resource: '*'
                Condition:
                  StringLike:
                    'kms:ViaService': 'codecommit.*.amazonaws.com'
      Tags:
        - Key: gitclassic:connection
          Value: !Ref ConnectionId
        - Key: gitclassic:template-version
          Value: '2'

  # Issues, comments and board status. Kept when this stack is deleted (Retain), so
  # turning editing off never loses issues; point-in-time recovery covers mistakes.
  IssuesTable:
    Type: AWS::DynamoDB::Table
    Condition: CreatesIssuesTable
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      TableName: !Sub 'gitclassic-issues-${ConnectionId}'
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: PK
          AttributeType: S
        - AttributeName: SK
          AttributeType: S
        - AttributeName: GSI1PK
          AttributeType: S
        - AttributeName: GSI1SK
          AttributeType: S
      KeySchema:
        - AttributeName: PK
          KeyType: HASH
        - AttributeName: SK
          KeyType: RANGE
      GlobalSecondaryIndexes:
        - IndexName: GSI1
          KeySchema:
            - AttributeName: GSI1PK
              KeyType: HASH
            - AttributeName: GSI1SK
              KeyType: RANGE
          Projection:
            ProjectionType: ALL
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true
      Tags:
        - Key: gitclassic:connection
          Value: !Ref ConnectionId

  # Only while ProtectedBranches is set: no direct commits to those branches.
  # An explicit Deny overrides the Allow above.
  ProtectedBranchesPolicy:
    Type: AWS::IAM::RolePolicy
    Condition: HasProtectedBranches
    Properties:
      RoleName: !Ref EditorRole
      PolicyName: gitclassic-editor-protected-branches
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: NoDirectCommitsToProtectedBranches
            Effect: Deny
            Action:
              - codecommit:PutFile
              - codecommit:CreateCommit
              - codecommit:DeleteFile
            Resource: '*'
            Condition:
              StringEqualsIfExists:
                'codecommit:References': !Split
                  - ','
                  - !Sub
                    - 'refs/heads/${Joined}'
                    - Joined: !Join [',refs/heads/', !Split [',', !Ref ProtectedBranches]]
              'Null':
                'codecommit:References': 'false'

Outputs:
  RoleArn:
    Description: The role GitClassic uses for changes. GitClassic finds it on its own; nothing to copy.
    Value: !GetAtt EditorRole.Arn
